#!/usr/bin/env python3 """ Claude Code PreToolUse hook — blocks Bash commands that READ or TRANSMIT secret files (.env*, .pgpass, ssh private keys). Why: `.claude/settings.json` permissions end with a blanket `Bash(*)` allow, which sidesteps the `Read(./.env*)` deny rule — `cat backend/.env`, `Get-Content .env`, `curl -F @backend/.env …` all exfiltrate prod DB passwords + FORGEJO/GLITCHTIP tokens without a single prompt. This hook closes that bypass at the command layer. Wired in `.claude/settings.json` hooks.PreToolUse with matcher "Bash". Behavior: - Reads hook input JSON from stdin; extracts tool_input.command. - Blocks (exit 2, stderr -> Claude) iff the command references a REAL secret file AND uses a read/transmit verb, OR `< file` redirection, OR `@file` upload, OR `open(`. - Template files (.env.example / .sample / .template / .dist / .md) are allowed. - `ls`/`stat`/`test -f` on a secret file are allowed (they don't read content). - Fail-OPEN on parse errors (exit 0): this is a tripwire layered on top of the deny list + (future) sandbox, not the sole guard — better to under-block than to wedge every Bash call on a malformed payload. """ from __future__ import annotations import json import re import sys # A secret-file token: `.env` with any number of dotted segments, `.pgpass`, or an # ssh private key. The leading delimiter ([start | space | quote | = : < @ ( / ]) # anchors it as a filename/path component, not a substring of another word. SECRET_RE = re.compile( r"(?:^|[\s'\"=:<@(/])" r"(\.env(?:\.[\w-]+)*|\.pgpass|id_rsa[\w.]*|id_ed25519[\w.]*|id_ecdsa[\w.]*)" r"(?![\w-])", re.IGNORECASE, ) TEMPLATE_SUFFIXES = (".example", ".sample", ".template", ".dist", ".md") # Verbs that read a file's content or push it off-box. READ_VERB_RE = re.compile( r"(?i)(? bool: low = token.lower() return any(low.endswith(s) for s in TEMPLATE_SUFFIXES) def main() -> int: try: payload = json.load(sys.stdin) except Exception: return 0 tool = payload.get("tool_name") or payload.get("tool") or "" if tool != "Bash": return 0 cmd = (payload.get("tool_input") or {}).get("command") or "" if not cmd: return 0 hits = [m.group(1) for m in SECRET_RE.finditer(cmd) if not _is_template(m.group(1))] if not hits: return 0 danger = ( READ_VERB_RE.search(cmd) or re.search(r"<\s*\S*\.env\b", cmd, re.IGNORECASE) or re.search(r"@\S*\.env\b", cmd, re.IGNORECASE) or re.search(r"open\(\s*['\"][^'\"]*\.env", cmd, re.IGNORECASE) ) if not danger: return 0 msg = [ f"Blocked: команда читает/передаёт секрет-файл ({hits[0]}).", "Blanket `Bash(*)` обходит `Read(./.env*)` deny — этот PreToolUse-hook закрывает дыру.", "Секрет-файлы содержат prod DB-пароли + FORGEJO/GLITCHTIP токены.", "Если правда нужно прочитать — сделай это вручную вне Claude, либо используй `.env.example`.", ] print("\n".join(msg), file=sys.stderr) return 2 if __name__ == "__main__": sys.exit(main())