Created
February 23, 2026 03:06
-
-
Save silenvx/5334afee5cd77e2dfcc1be4e943ad43b to your computer and use it in GitHub Desktop.
PermissionRequest hook: 複数行コマンドの自動承認 — https://zenn.dev/ux_xu/articles/4f57169b0dd820
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """PermissionRequest hook: 安全なコマンドパターンを自動承認する. | |
| 改行を含むコマンドが permissions.allow のパターンにマッチしない問題の | |
| ワークアラウンド (https://github.com/anthropics/claude-code/issues/11932). | |
| """ | |
| import json | |
| import re | |
| import sys | |
| try: | |
| data = json.load(sys.stdin) | |
| except (json.JSONDecodeError, ValueError): | |
| sys.exit(0) | |
| command = data.get("tool_input", {}).get("command", "") | |
| # Claude Code が付与する 2>&1 / 2>&1 || echo "---EXIT: $?" を除去してから評価 | |
| # パイプ前・行継続前・末尾のいずれの位置でも除去 | |
| command = re.sub( | |
| r"\s+2>&1(?:\s+\|\|\s+echo\s+\"---EXIT:\s+\$\?\"\s*)?(?=\s*[\\|]|$)", | |
| "", command, | |
| ) | |
| # シェル行継続 (backslash + newline + 任意の水平空白) を単一スペースに正規化 | |
| command = re.sub(r'\\\n[ \t]*', ' ', command) | |
| # || true / || : は終了ステータスを 0 にするだけで副作用なし | |
| command = re.sub(r'\s*\|\|\s*(?:true|:)\s*$', '', command) | |
| # パス文字の allowlist (シェルメタ文字を排除) | |
| _SAFE_PATH_CHARS = r'[\w./@:~+-]+' | |
| _SAFE_PATH = rf'(?:"[\w\s./@:~+-]+"|{_SAFE_PATH_CHARS})' | |
| # cd <safe-path> && のオプショナルプレフィックス | |
| _CD_PREFIX = rf'(?:cd\s+{_SAFE_PATH}\s*&&\s*)?' | |
| # git add <files> && のオプショナルプレフィックス | |
| _GIT_ADD_PREFIX = ( | |
| rf'(?:git\s+(?:-C\s+{_SAFE_PATH}\s+)?' | |
| r'add\s+[^\n\r\x00&|;$`<>()]+\s*&&\s*)?' | |
| ) | |
| _GIT_COMMIT_BASE = ( | |
| r'git\s+' | |
| rf'(?:-C\s+{_SAFE_PATH}\s+)?' | |
| r'commit' | |
| r'(?:\s+--(?!amend\b)[\w-]+)*' | |
| ) | |
| _GIT_STATUS_SUFFIX = ( | |
| r'(?:[ \t]*&&[ \t]*git[ \t]+' | |
| rf'(?:-C[ \t]+{_SAFE_PATH}[ \t]+)?' | |
| r'status(?:[ \t]+[\w./@:~+-]*)*)?' | |
| r'[ \t]*$' | |
| ) | |
| # AI CLI: SQ 状態を文字単位で追跡しクォート外のメタ文字を拒否 | |
| _CODEX_SANDBOX = r'(?:(?:-s|--sandbox)[ \t]+[\w-]+[ \t]+)?' | |
| _AI_CLI_PREFIX = re.compile( | |
| r'^(?:' | |
| rf'codex[ \t]+exec[ \t]+--skip-git-repo-check[ \t]+{_CODEX_SANDBOX}' | |
| rf'|codex[ \t]+--cd[ \t]+{_SAFE_PATH_CHARS}[ \t]+exec[ \t]+--skip-git-repo-check[ \t]+{_CODEX_SANDBOX}' | |
| r'|gemini[ \t]+(?:-s[ \t]+)?-p[ \t]+' | |
| rf'|cursor-agent[ \t]+-p[ \t]+(?:--trust[ \t]+)?(?:--model[ \t]+[\w.-]+[ \t]+)?(?:--workspace[ \t]+[/~.]{_SAFE_PATH_CHARS}[ \t]+)?' | |
| r')', | |
| re.ASCII, | |
| ) | |
| _AI_CLI_SUFFIX = re.compile( | |
| r'(?:[ \t]+--[\w-]+(?:[ \t]+[\w./@:~+,-]+)?)*[ \t]*$', | |
| re.ASCII, | |
| ) | |
| # SQ 外で危険なシェルメタ文字 (# はコメント開始によるコマンド切り捨て) | |
| _SQ_OUTSIDE_DANGER = re.compile(r'[;\n\r\x00&|$`<>(){}#]') | |
| def _is_safe_ai_cli(command: str) -> bool: | |
| """AI CLI (codex/gemini/cursor-agent) コマンドの安全性をパーサーベースで検証. | |
| SQ 内はリテラルで安全。クォート外の文字列にシェルメタ文字が | |
| 含まれていないことを検証する。'\'' イディアムもサポート。 | |
| """ | |
| m = _AI_CLI_PREFIX.match(command) | |
| if not m: | |
| return False | |
| rest = command[m.end():] | |
| if not rest or rest[0] != "'": | |
| return False | |
| in_sq = False | |
| i = 0 | |
| last_close = -1 | |
| while i < len(rest): | |
| ch = rest[i] | |
| if ch == "'": | |
| if not in_sq: | |
| in_sq = True | |
| i += 1 | |
| else: | |
| in_sq = False | |
| last_close = i | |
| # '\'' idiom: close-quote + \' + open-quote | |
| if i + 2 < len(rest) and rest[i + 1 : i + 3] == "\\'": | |
| i += 3 | |
| if i < len(rest) and rest[i] == "'": | |
| in_sq = True | |
| i += 1 | |
| # '"'"' idiom: close-quote + "'" + open-quote | |
| elif (i + 4 < len(rest) | |
| and rest[i + 1 : i + 5] == "\"'\"'"): | |
| in_sq = True | |
| i += 5 | |
| else: | |
| i += 1 | |
| elif not in_sq: | |
| if _SQ_OUTSIDE_DANGER.search(ch): | |
| return False | |
| i += 1 | |
| else: | |
| i += 1 | |
| if in_sq: | |
| return False | |
| return bool(_AI_CLI_SUFFIX.match(rest[last_close + 1 :])) | |
| # --help / --version で終わるコマンドは副作用なし (permissions.allow の * --help 相当) | |
| # サフィックス除去済みのコマンドに対して評価される | |
| _SAFE_HELP_VERSION = re.compile( | |
| r'^[\w][\w./-]*(?:\s+[\w][\w./@:~+-]*)*\s+(?:--help|--version)\s*$', | |
| re.ASCII, | |
| ) | |
| # npm audit: fix サブコマンドを除外し、フラグと signatures のみ許可 | |
| _SAFE_NPM_AUDIT = re.compile( | |
| r'^npm\s+audit(?:\s+signatures)?(?:\s+--[\w-]+(?:=\S+)?)*\s*$', | |
| re.ASCII, | |
| ) | |
| # ホワイトリスト: コマンド全体が期待される構造に完全一致する場合のみ承認 | |
| # $ アンカーにより末尾に ; | && 等があれば不一致 → 拒否 | |
| PATTERNS = [ | |
| # [cd <path> &&] [git add ... &&] git [-C <path>] commit -m "$(cat <<'EOF'\n...\nEOF\n)" | |
| # (?:(?!\)").)*? でコンテンツ内の )" (heredoc 脱出) を禁止し二重 EOF injection を防止 | |
| re.compile( | |
| rf'^{_CD_PREFIX}{_GIT_ADD_PREFIX}' | |
| rf'{_GIT_COMMIT_BASE}' | |
| r'\s+-m\s+"\$\(cat\s+<<\'EOF\'\n' | |
| r'(?:(?!\)").)*?' | |
| rf'\n[ \t]*EOF\n[ \t]*\)"{_GIT_STATUS_SUFFIX}', | |
| re.DOTALL, | |
| ), | |
| # [cd <path> &&] [git add ... &&] git [-C <path>] commit -m "直接複数行メッセージ" | |
| # [^"$`]* でダブルクォート内のコマンド置換を防ぐ (heredoc 'EOF' は展開されないため不要) | |
| re.compile( | |
| rf'^{_CD_PREFIX}{_GIT_ADD_PREFIX}' | |
| rf'{_GIT_COMMIT_BASE}' | |
| rf'\s+-m\s+"[^"$`]*"{_GIT_STATUS_SUFFIX}', | |
| re.DOTALL, | |
| ), | |
| # for <var> in <paths>; do echo "$(wc -l < "$<var>") $<var>"; done [| sort -n] | |
| # 後方参照 \1 でループ変数の一致を検証し、ボディ構造を固定 | |
| # re.ASCII: \w を [a-zA-Z0-9_] に限定しシェル変数名定義と一致させる | |
| re.compile( | |
| r'^for\s+(\w+)\s+in' | |
| rf'(?:\s+{_SAFE_PATH_CHARS})+' | |
| r';\s*do\s+echo\s+"\$\(wc\s+-l\s+<\s+"\$\1"\)\s+\$\1";\s*done' | |
| r'(?:\s*\|\s*sort\s+-[nr]+)?\s*$', | |
| re.ASCII, | |
| ), | |
| ] | |
| # 安全な読み取り専用コマンドの複合パイプライン自動承認 | |
| # \s は \n を含みシェル制御文字として悪用されるため、水平空白 [ \t] のみ使用 | |
| _RO_CMDS = ( | |
| 'basename', 'dirname', 'echo', 'file', 'id', 'ls', 'pwd', | |
| 'readlink', 'sleep', 'stat', 'tree', 'uname', 'wc', 'which', 'whoami', | |
| ) | |
| _RO_CMD = r'(?:' + '|'.join(_RO_CMDS) + r')' | |
| # re.ASCII: \w を [a-zA-Z0-9_] に制限しシェル変数名定義と一致させる | |
| _RO_BARE = r'[\w./@:~+,=-]+' | |
| _RO_DQ = r'"(?:[^"$`\\]|\$\?)*"' | |
| _RO_SQ = r"'[^']*'" | |
| _RO_ARG = rf'(?:{_RO_BARE}|{_RO_DQ}|{_RO_SQ})' | |
| _RO_REDIR = r'(?:[ \t]+2>(?:&1|[ \t]*/dev/null))?' | |
| _RO_SEG = rf'{_RO_CMD}(?:[ \t]+{_RO_ARG})*{_RO_REDIR}' | |
| # パイプフィルタ: stdin のみ読み取り、ファイル引数不可 | |
| # 短フラグ (-n, -rn, -5) と数値 (5, 2,3) のみ許可しパス引数を排除 | |
| # 長フラグ (--output=file) は先頭 -- の2文字目が [a-zA-Z0-9] 外のため不一致 | |
| _PF_CMD = r'(?:base64|head|tail|sort|wc)' | |
| _PF_ARG = r'(?:-[a-zA-Z0-9]+|[\d,]+)' | |
| _PF = rf'{_PF_CMD}(?:[ \t]+{_PF_ARG})*{_RO_REDIR}' | |
| # macOS (BSD) base64 は -i <file> / -o <file> でファイル I/O 可能 | |
| # getopt 仕様により -ifoo / -ofoo (スペースなし) も有効 | |
| _BASE64_DANGEROUS_RE = re.compile( | |
| r'(?:^|[ \t])(?:-[a-zA-Z]*[io][a-zA-Z]*|--input\b|--output\b)', | |
| re.ASCII, | |
| ) | |
| # npm audit パイプライン: _GPF 定義後に宣言 (grep/sed もパイプフィルタとして許可) | |
| def _is_safe_readonly_compound(command: str) -> bool: | |
| """RO/cd/git read-only/git mv コマンドの &&/|| チェーン安全性を検証.""" | |
| if not _SAFE_COMPOUND_CHAIN.match(command): | |
| return False | |
| parts = _split_pipe(command) | |
| main_chain = parts[0] | |
| for seg in _split_chain(main_chain): | |
| seg = seg.strip() | |
| if seg.startswith('git') and _GIT_DANGEROUS_RE.search(seg): | |
| return False | |
| if _has_dangerous_pipe_filter(parts[1:]): | |
| return False | |
| return True | |
| # Git 読み取り専用パイプライン自動承認 | |
| # git show/diff/log の出力を安全なフィルタにパイプする構造のみ許可 | |
| # &&/|| は禁止(パイプ | のみ) | |
| _GIT_RO_SUBCMDS = ( | |
| r'(?:show|diff|log|ls-tree' | |
| r'|blame|describe|for-each-ref|ls-files' | |
| r'|name-rev|rev-list|rev-parse|shortlog)' | |
| ) | |
| _GIT_DANGEROUS_RE = re.compile( | |
| r'(?:^|\s)(?:--output\b|--ext-diff\b|--exec-path\b|--pager\b|--contents\b|--no-index\b)', | |
| re.ASCII, | |
| ) | |
| _GIT_FLAG_VAL = rf"[\w./@:~+,=%-]*(?:{_RO_SQ}|{_RO_DQ})?[\w./@:~+,=%-]*" | |
| _GIT_RO_FLAG = rf'--?[\w][\w-]*(?:={_GIT_FLAG_VAL})?' | |
| _GIT_RO_SEG = ( | |
| r'git' | |
| rf'(?:[ \t]+-C[ \t]+{_RO_ARG})?' | |
| rf'[ \t]+{_GIT_RO_SUBCMDS}' | |
| rf'(?:[ \t]+(?:{_GIT_RO_FLAG}|{_RO_ARG}))*' | |
| rf'{_RO_REDIR}' | |
| ) | |
| _SED_FLAG = r'-[ne]+' | |
| _SED_SCRIPT = rf'(?:{_RO_SQ}|{_RO_DQ})' | |
| _SED_ATOM = rf'(?:{_SED_FLAG}|{_SED_SCRIPT})' | |
| _SED_SEG = rf'sed(?:[ \t]+{_SED_ATOM})+' | |
| _GREP_FLAGS = r'-[EeivconlPqxswHhoABCm\d]+' | |
| _GREP_FARG = rf'(?:{_GREP_FLAGS}(?:[ \t]+[\d]+)?)' | |
| # grep BRE/ERE パターンはバックスラッシュを多用する (\|, \(, \+ 等) | |
| # シェル DQ 内の \ は "$`"\ の前でのみ特殊なため \| 等はリテラルで安全 | |
| # \\. は \x → 2文字消費し、\" で DQ 境界を誤認しない | |
| # sed の _SED_FILE_IO 抽出器は _RO_DQ 前提のため、この緩和は grep 専用に限定 | |
| _GREP_DQ = r'"(?:[^"$`\\]|\\.)*"' | |
| # パターン位置に -- ロングオプションが来ないよう負の先読みで排除 | |
| _GREP_PATTERN = rf'(?:{_RO_SQ}|{_GREP_DQ}|(?!--){_RO_BARE})' | |
| _GREP_SEG = ( | |
| r'grep' | |
| rf'(?:[ \t]+{_GREP_FARG})*' | |
| rf'(?:[ \t]+{_GREP_PATTERN})?' | |
| ) | |
| _GPF = rf'(?:{_PF}|{_SED_SEG}|{_GREP_SEG})' | |
| # npm audit パイプライン: 安全なフィルタへのパイプのみ許可 (&&/|| は禁止) | |
| _NPM_AUDIT_PIPELINE = re.compile( | |
| r'^npm[ \t]+audit(?:[ \t]+signatures)?(?:[ \t]+--[\w-]+(?:=\S+)?)*' | |
| rf'(?:[ \t]*\|[ \t]*{_GPF})+[ \t]*$', | |
| re.ASCII, | |
| ) | |
| _GIT_RO_PIPELINE = re.compile( | |
| rf'^[ \t]*{_GIT_RO_SEG}' | |
| rf'(?:[ \t]*\|[ \t]*{_GPF})*' | |
| r'[ \t]*$', | |
| re.ASCII, | |
| ) | |
| _SED_FILE_IO = re.compile(r'[wWrRe][ \t]', re.ASCII) | |
| # sed 置換コマンドの e フラグ: s/pat/repl/e でパターンスペースをシェル実行 | |
| _SED_SUBST_E = re.compile(r's(.).*?\1.*?\1\w*e', re.ASCII) | |
| def _split_pipe(command: str) -> list[str]: | |
| """クォート外の | でコマンドをパイプセグメントに分割.""" | |
| segments = [] | |
| current: list[str] = [] | |
| in_sq = in_dq = False | |
| i = 0 | |
| while i < len(command): | |
| c = command[i] | |
| if c == "'" and not in_dq: | |
| in_sq = not in_sq | |
| current.append(c) | |
| i += 1 | |
| elif c == '\\' and in_dq and i + 1 < len(command): | |
| # DQ 内の \x → 2文字消費 (\" で DQ 境界を誤認しない) | |
| current.append(c) | |
| current.append(command[i + 1]) | |
| i += 2 | |
| elif c == '"' and not in_sq: | |
| in_dq = not in_dq | |
| current.append(c) | |
| i += 1 | |
| elif c == '|' and not in_sq and not in_dq: | |
| segments.append(''.join(current)) | |
| current = [] | |
| i += 1 | |
| else: | |
| current.append(c) | |
| i += 1 | |
| segments.append(''.join(current)) | |
| return segments | |
| def _split_chain(command: str) -> list[str]: | |
| """クォート外の &&/||/; でコマンドをチェーンセグメントに分割.""" | |
| segments = [] | |
| current: list[str] = [] | |
| in_sq = in_dq = False | |
| i = 0 | |
| while i < len(command): | |
| ch = command[i] | |
| if ch == "'" and not in_dq: | |
| in_sq = not in_sq | |
| current.append(ch) | |
| i += 1 | |
| elif ch == '\\' and in_dq and i + 1 < len(command): | |
| # DQ 内の \x → 2文字消費 (\" で DQ 境界を誤認しない) | |
| current.append(ch) | |
| current.append(command[i + 1]) | |
| i += 2 | |
| elif ch == '"' and not in_sq: | |
| in_dq = not in_dq | |
| current.append(ch) | |
| i += 1 | |
| elif not in_sq and not in_dq: | |
| if ch == ';': | |
| segments.append(''.join(current)) | |
| current = [] | |
| i += 1 | |
| elif (ch == '&' and i + 1 < len(command) | |
| and command[i + 1] == '&'): | |
| segments.append(''.join(current)) | |
| current = [] | |
| i += 2 | |
| elif (ch == '|' and i + 1 < len(command) | |
| and command[i + 1] == '|'): | |
| segments.append(''.join(current)) | |
| current = [] | |
| i += 2 | |
| else: | |
| current.append(ch) | |
| i += 1 | |
| else: | |
| current.append(ch) | |
| i += 1 | |
| segments.append(''.join(current)) | |
| return segments | |
| def _check_sed_script(content: str) -> bool: | |
| """sed スクリプト内容が危険なら True を返す.""" | |
| if _SED_FILE_IO.search(content): | |
| return True | |
| if _SED_SUBST_E.search(content): | |
| return True | |
| return False | |
| def _has_dangerous_pipe_filter(segments: list[str]) -> bool: | |
| """パイプセグメント内の sed/base64 が危険なら True を返す.""" | |
| for seg in segments: | |
| stripped = seg.strip() | |
| if stripped.startswith('sed'): | |
| for m in re.finditer(r"'([^']*)'", stripped): | |
| if _check_sed_script(m.group(1)): | |
| return True | |
| for m in re.finditer(r'"([^"$`\\]*)"', stripped): | |
| if _check_sed_script(m.group(1)): | |
| return True | |
| if stripped.startswith('base64'): | |
| if _BASE64_DANGEROUS_RE.search(stripped): | |
| return True | |
| return False | |
| # gh api 読み取り専用パイプライン自動承認 | |
| # 書き込みフラグがない gh api GET リクエストのパイプラインのみ許可 | |
| # &&/|| は禁止(パイプ | のみ) | |
| _GH_API_DANGEROUS_RE = re.compile( | |
| r'(?:^|[ \t])' | |
| r'(?:-X[ \t]*\S|--method\b|-[fF]|--raw-field\b|--field\b|--input\b|--hostname\b)', | |
| re.ASCII, | |
| ) | |
| _GH_API_GRAPHQL_RE = re.compile( | |
| r'gh[ \t]+api[ \t]+["\']*/?graphql\b', re.ASCII, | |
| ) | |
| # jq パイプセグメント: ファイル読み取りフラグ (-f/--from-file, --slurpfile, --rawfile) をブロック | |
| _JQ_DANGEROUS_RE = re.compile( | |
| r'(?:^|[ \t])(?:-[fL]|--from-file\b|--slurpfile\b|--rawfile\b|--include-path\b)', | |
| re.ASCII, | |
| ) | |
| _JQ_FLAG = r'-[a-zA-Z0-9]+' | |
| _JQ_FILTER = rf'(?:{_RO_SQ}|{_RO_DQ}|\.[^ \t|]*)' | |
| _JQ_SEG = rf'jq(?:[ \t]+{_JQ_FLAG})*(?:[ \t]+{_JQ_FILTER})?' | |
| _GH_API_BARE_EP = r'/?[a-zA-Z0-9_][a-zA-Z0-9_.{}/~:+?=-]*' | |
| _GH_API_ENDPOINT = rf'(?:{_RO_DQ}|{_RO_SQ}|{_GH_API_BARE_EP})' | |
| _GH_API_FLAG = rf'--?[\w][\w-]*(?:[= \t]+{_RO_ARG})?' | |
| _GH_API_RO_SEG = ( | |
| r'gh[ \t]+api' | |
| rf'[ \t]+{_GH_API_ENDPOINT}' | |
| rf'(?:[ \t]+{_GH_API_FLAG})*' | |
| rf'{_RO_REDIR}' | |
| ) | |
| _GHPF = rf'(?:{_GPF}|{_JQ_SEG})' | |
| _GH_API_RO_PIPELINE = re.compile( | |
| rf'^[ \t]*{_GH_API_RO_SEG}' | |
| rf'(?:[ \t]*\|[ \t]*{_GHPF})*' | |
| r'[ \t]*$', | |
| re.ASCII, | |
| ) | |
| # 安全なコマンドチェーン自動承認 (RO commands, cd, git read-only, git mv の &&/|| チェーン) | |
| _CD_SEG = rf'cd[ \t]+{_RO_ARG}' | |
| _GIT_COMPOUND_SUBCMDS = ( | |
| r'(?:show|diff|log|status|ls-tree' | |
| r'|blame|describe|for-each-ref|ls-files' | |
| r'|name-rev|rev-list|rev-parse|shortlog)' | |
| ) | |
| _GIT_COMPOUND_SEG = ( | |
| r'git' | |
| rf'(?:[ \t]+-C[ \t]+{_RO_ARG})?' | |
| rf'[ \t]+{_GIT_COMPOUND_SUBCMDS}' | |
| rf'(?:[ \t]+(?:{_GIT_RO_FLAG}|{_RO_ARG}))*' | |
| rf'{_RO_REDIR}' | |
| ) | |
| # git mv: 非破壊的なリネーム操作 (git commit より低リスク) | |
| # フラグは公式オプション4つのみ: -f(force), -n(dry-run), -k(skip), -v(verbose) | |
| _GIT_MV_SEG = ( | |
| r'git' | |
| rf'(?:[ \t]+-C[ \t]+{_RO_ARG})?' | |
| r'[ \t]+mv' | |
| rf'(?:[ \t]+-[fnkv])*' | |
| rf'(?:[ \t]+{_RO_ARG}){{2,}}' | |
| ) | |
| _COMPOUND_SEG = rf'(?:{_RO_SEG}|{_CD_SEG}|{_GIT_COMPOUND_SEG}|{_GIT_MV_SEG})' | |
| _SAFE_COMPOUND_CHAIN = re.compile( | |
| rf'^[ \t]*{_COMPOUND_SEG}' | |
| rf'(?:[ \t]+(?:&&|\|\|)[ \t]+{_COMPOUND_SEG}|[ \t]*;[ \t]*{_COMPOUND_SEG})*' | |
| rf'(?:[ \t]*\|[ \t]*{_PF})*[ \t]*$', | |
| re.ASCII, | |
| ) | |
| def _is_safe_npm_audit_pipeline(command: str) -> bool: | |
| """npm audit パイプラインの安全性を検証.""" | |
| if not _NPM_AUDIT_PIPELINE.match(command): | |
| return False | |
| parts = _split_pipe(command) | |
| return not _has_dangerous_pipe_filter(parts[1:]) | |
| def _is_safe_git_readonly_pipeline(command: str) -> bool: | |
| """git read-only コマンドのパイプライン安全性を検証.""" | |
| if not _GIT_RO_PIPELINE.match(command): | |
| return False | |
| parts = _split_pipe(command) | |
| if _GIT_DANGEROUS_RE.search(parts[0]): | |
| return False | |
| if _has_dangerous_pipe_filter(parts[1:]): | |
| return False | |
| return True | |
| def _is_safe_gh_api_pipeline(command: str) -> bool: | |
| """gh api read-only コマンドのパイプライン安全性を検証.""" | |
| if not _GH_API_RO_PIPELINE.match(command): | |
| return False | |
| parts = _split_pipe(command) | |
| if _GH_API_GRAPHQL_RE.search(parts[0]): | |
| return False | |
| if _GH_API_DANGEROUS_RE.search(parts[0]): | |
| return False | |
| if _has_dangerous_pipe_filter(parts[1:]): | |
| return False | |
| for seg in parts[1:]: | |
| stripped = seg.strip() | |
| if stripped.startswith('jq'): | |
| if _JQ_DANGEROUS_RE.search(stripped): | |
| return False | |
| return True | |
| if ( | |
| _is_safe_ai_cli(command) | |
| or _SAFE_NPM_AUDIT.match(command) | |
| or _is_safe_npm_audit_pipeline(command) | |
| or _SAFE_HELP_VERSION.match(command) | |
| or any(p.match(command) for p in PATTERNS) | |
| or _is_safe_readonly_compound(command) | |
| or _is_safe_git_readonly_pipeline(command) | |
| or _is_safe_gh_api_pipeline(command) | |
| ): | |
| json.dump( | |
| { | |
| "hookSpecificOutput": { | |
| "hookEventName": "PermissionRequest", | |
| "decision": {"behavior": "allow"}, | |
| } | |
| }, | |
| sys.stdout, | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment