Claude Code auto 모드로 옮기며 권한 설정을 다시 짠 작업기
Claude Code를 auto 모드로 쓰는 사람, 혹은 기본값이 바뀌어 auto로 시작하게 된 사람을 위한 글이다. 기존 permissions.allow·deny가 auto에서 어떻게 다시 해석되는지와, 이를 점검하는 짧은 스크립트를 정리했다. 결론은 이렇다. 넓게 열어 둔 allow는 꺼지고, 좁지만 파괴적인 allow는 분류기를 건너뛰어 그대로 통과한다. 막고 싶은 규칙은 분류기에 실제로 닿는 자리에 둬야 한다.
bypass에서는 allow 목록을 검증할 기회가 없었다
permission mode는 Claude가 묻지 않고 할 수 있는 범위를 정하는 세션 설정이다. bypassPermissions는 거의 모든 확인을 건너뛴다. auto에서는 별도 모델인 분류기(classifier)가 사람 대신 도구 호출 하나하나를 위험과 프롬프트 인젝션 관점에서 판정한다. permissions.allow·deny는 Bash(git commit *)처럼 도구 이름과 명령 접두사를 문자열로 적는 규칙이다.
나는 bypass로 써 왔다. bypass에서는 allow 룰이 아무 효과가 없다. 그래서 user-level settings.json의 allow 30줄은 맞는지 틀린지 드러날 일 없이 쌓여 있었다. auto로 옮기면서 처음으로 이 목록을 판정 순서에 대입해 봤다. 2026년 10월 기준 공식 문서에 따르면 v2.1.283부터 터미널과 VS Code 세션은 따로 지정하지 않으면 auto로 시작한다. bypass를 쓰지 않던 사람도 같은 재해석을 겪는다는 얘기다.
판정 순서가 규칙의 생사를 정한다

auto에서 호출 하나는 deny·ask → allow → 작업 디렉터리 안 읽기·편집 → 분류기 순서로 내려간다. 먼저 걸린 단계가 결론을 내고, 뒤 단계는 그 호출을 보지 않는다. 또 auto에 들어가는 순간 임의 코드 실행을 허용하는 넓은 allow는 꺼진다.
내 목록에 대입해 보니 의도와 반대로 움직였다.
Bash(pnpm run *),Bash(npm run *)는 패키지 매니저 run이라 꺼진다. 테스트와 빌드를 묻지 않고 돌리려고 넣은 줄이 아무 일도 하지 않았다. 이 의도는autoMode.environment(분류기에게 내 작업 환경을 설명하는 문장 목록)에 옮겨 적었다.- 경로 범위가 없는
Edit·Write두 줄은 지웠다. 작업 디렉터리 안 편집은 auto가 원래 자동 승인한다. 남는 효과는 디렉터리 밖 편집을 분류기 없이 통과시키는 것뿐이었다. allow는 28줄이 됐다. - 반대로
Bash(git add *),Bash(git commit *),Bash(git reset *)는 범위가 좁아서 살아남는다. 살아남은 allow는 분류기보다 먼저 결론을 내기 때문에, 이 셋은 인자가 무엇이든 프롬프트 없이 실행된다. 전체 staging, 커밋, 이력 되돌리기처럼 되돌리기 가장 어려운 명령이 분류기를 거치지 않는 셈이다.
넓게 열어 둔 줄은 꺼지고 좁고 위험한 줄은 남았다. "auto니까 분류기가 다 본다"는 감각은 여기서 틀린다.
문자열 deny는 적어 둔 철자만 막는다
전체 staging을 막으려고 deny에 git add -A, git add --all, git add ., -u 계열까지 일곱 줄을 적어 두었다. 철자를 나열하는 방식이라 공백이나 옵션 순서가 바뀌거나, 인터프리터 인라인 실행이나 스크립트를 거치면 매칭에서 빠진다(구체적인 우회 철자는 적지 않는다). 공식 문서도 Bash(git push *) 같은 규칙은 그 접두사로 시작하는 명령만 잡는다고 설명한다.
auto에는 의미로 판정하는 슬롯이 있다. autoMode.soft_deny(사용자가 명시적으로 지시하면 풀리는 차단)와 autoMode.hard_deny(무조건 차단)에 규칙을 문장으로 적으면 분류기가 읽는다. 새 정책을 만들지는 않았다. 문서에 흩어져 있던 계약을 설정으로 옮겼을 뿐이다. 폴더별 git 계정 분리, AI 서명 금지, 보호 브랜치 직접 push 금지, 다른 저장소를 복사해 둔 미러 디렉터리 직접 편집 금지가 그것이다. 공개할 수 있게 줄이면 이렇다.
{
"permissions": { "defaultMode": "auto" },
"autoMode": {
"environment": [
"$defaults",
"Commit/push/PR only via user-run slash commands.",
"pnpm/npm run for tests and builds is routine."
],
"soft_deny": [
"$defaults",
"Pushing directly to `main` or `develop`.",
"Editing the config mirror dir; edit its source."
],
"hard_deny": [
"$defaults",
"Changing git credential helper or gh account.",
"AI attribution in commits/PRs, even if asked."
]
}
}
놓치기 쉬운 조건이 둘 있다. 배열마다 "$defaults"가 없으면 force push나 curl | bash 차단 같은 내장 규칙이 통째로 교체된다. 그리고 autoMode는 user 설정(~/.claude/settings.json), managed 설정, --settings 플래그에서만 읽힌다. 프로젝트의 .claude/settings.json에 넣으면 무시된다.
문장으로 쓴 규칙도 분류기에 닿아야 작동한다
여기서 한 번 더 걸렸다. 위 hard_deny의 AI 서명 금지는 커밋 메시지를 봐야 판정된다. 그런데 Bash(git commit *) allow가 먼저 결론을 내 버리니 분류기는 그 커밋을 아예 보지 못한다. 같은 구조를 팀 공유 설정으로 옮기면서 확인한 사실이다. 서명을 실제로 막는 건 분류기가 아니라 settings.json의 attribution 블록(commit·pr을 빈 문자열로 지정)이다. 문장 규칙은 allow에 가려지지 않는 호출, 지금 내 목록에서는 git push 정도에만 기대할 수 있다.
손으로 대조하기 번거로워서 스크립트로 만들었다. Node 18 이상에서 의존성 없이 돈다.
// audit-auto-allow.mjs
// auto 모드에서 allow 룰이 실제로 어떻게
// 동작하는지 분류한다.
// 사용: node audit-auto-allow.mjs [path]
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
const file = process.argv[2]
?? `${homedir()}/.claude/settings.json`;
const s = JSON.parse(readFileSync(file, 'utf8'));
const allow = s.permissions?.allow ?? [];
const deny = s.permissions?.deny ?? [];
const am = s.autoMode ?? {};
const prose = [
...(am.soft_deny ?? []),
...(am.hard_deny ?? []),
].filter((r) => r !== '$defaults');
// 문서상 auto 진입 시 꺼지는 넓은 룰
const DROPPED = [
/^(Bash|PowerShell)\(\*\)$/,
/^Bash\((python|node|ruby|perl|sh|bash)\S*\*/,
/^Bash\((npm|pnpm|yarn|bun) run/,
/^(Agent|Monitor)(\(|$)/,
];
// 살아남지만 되돌리기 어려운 동작
const RISKY = new RegExp(
'^Bash\\((git (add|commit|reset|push|rebase'
+ '|clean|checkout)|rm |gh pr)',
);
const prefixOf = (r) =>
r.match(/^Bash\((.+?)\s?\*?\)$/)?.[1];
for (const r of allow) {
if (DROPPED.some((re) => re.test(r))) {
console.log(`DEAD ${r}`);
} else if (RISKY.test(r)) {
console.log(`BYPASS ${r}`);
}
}
// 문장 규칙의 백틱 명령이 allow에 먼저 걸리는지
for (const rule of prose) {
const cmds = [...rule.matchAll(/`([^`]+)`/g)]
.map((m) => m[1]);
for (const c of cmds) {
const by = (rules) => rules.find((r) => {
const p = prefixOf(r);
return p && c.startsWith(p);
});
const hit = by(allow);
// deny가 먼저 막는 철자는 문제없다
if (hit && !by(deny)) {
console.log(`SHADOW "${c}" <- ${hit}`);
}
}
}
DEAD는 auto에서 꺼진 줄, BYPASS는 분류기를 건너뛰는 위험 명령, SHADOW는 문장 규칙에 적은 명령이 deny보다 allow에 먼저 걸리는 경우다. 내 설정으로 돌린 결과는 이렇다.
DEAD Bash(pnpm run *)
DEAD Bash(npm run *)
BYPASS Bash(gh pr *)
BYPASS Bash(git add *)
BYPASS Bash(git commit *)
BYPASS Bash(git reset *)
SHADOW "git worktree remove" <- Bash(git worktree *)
이 출력은 판정까지 해 주지 않는 후보 목록이다. 정규식에는 내가 쓰는 명령만 넣었고, SHADOW는 백틱 안의 명령만 본다. 마지막 줄은 규칙이 금지 대상 대신 허용 경로로 언급한 명령이라 오탐에 가깝다. 그래도 git worktree remove가 분류기 없이 실행된다는 사실 자체는 맞다. BYPASS를 줄이는 방법은 두 가지다. allow를 더 좁히거나, autoMode.classifyAllShell: true로 모든 셸 명령을 분류기에 보내는 것이다. 후자는 명령마다 분류기 호출이 붙어 응답이 느려진다.
설정 커맨드는 보이지 않았고, 설정 파일은 사람이 고친다
/auto-mode-setup은 environment 초안을 써 주는 커맨드인데 내 목록에는 뜨지 않았다. 로컬 설정이나 환경변수 문제가 아니었다. 서버 쪽 플래그가 노출을 정하는 커맨드였다. 막힌 건 노출뿐이라 autoMode 블록을 손으로 써넣으면 정상 동작한다. 이 커맨드의 사전 스캔은 셸 히스토리의 명령어 첫 단어와 홈 디렉터리 아래 저장소 목록을 읽는다. 지금 문서에는 두 스캔이 실행 전에 동의를 묻는 선택 항목으로 적혀 있지만 편의 기능이 무엇을 입력으로 가져가는지는 직접 확인하고 돌리는 편이 낫다.
auto 모드의 Claude는 ~/.claude/settings.json을 고치지 못한다. 분류기가 [Self-Modification]으로 막는다. 권한을 넓히는 변경은 사람 손을 거쳐야 한다는 뜻이다.
마무리
auto 모드는 기존 allow 목록을 다시 해석한다. 넓은 룰은 꺼지고, 좁은 룰은 분류기보다 먼저 결론을 낸다. 규칙을 문서에서 설정으로 옮기는 일과, 그 규칙이 실제 판정 경로에 놓였는지 확인하는 일은 따로 해야 한다.
지금 node audit-auto-allow.mjs를 돌려 BYPASS 줄부터 "이 명령을 분류기 없이 실행해도 되는가"를 하나씩 따져 보자.
참고