# Claude Safety Hook (FNDG-04b) ## Purpose Sessions launched by claude-failover run with `--dangerously-skip-permissions` so the dispatcher pool (`ccl-auto-11`–`ccl-auto-20`) stays headless. That flag suppresses the interactive confirmation for every tool call. Combined with the MCP `sessions.send` endpoint, a prompt injection could otherwise execute arbitrary destructive commands with zero friction. This package is the **second line of defence** — the first is input sanitization in `secuaas-mcp` (FNDG-04). The hook applies a deny-list of destructive Bash patterns and write paths to every tool call the agent attempts, and exits non-zero to block them. Decision: **Option A** (per Olivier, 2026-04-19). Option B (removing the flag) was rejected — it breaks headless dispatch. Option C (two pools) is deferred to a separate task. ## Mechanism ``` ┌──────────────────┐ ┌──────────────────────────┐ ┌──────────────────────┐ │ claude-failover │ │ project dir │ │ claude (TUI) │ │ launchAgent() │──1──▶│ .claude/hooks/ │ │ reads settings.json │ │ │ │ claude-safety-hook.sh │◀──3──│ runs hook per tool │ │ EnsureHook- │ │ .claude/settings.json │ │ │ │ Deployed() │──2──▶│ PreToolUse → hook │ │ │ └──────────────────┘ └──────────────────────────┘ └──────────────────────┘ │ │ └──────────────────────── 4. tmux SendKeys claude ─────────────┘ ``` 1. Before each claude launch, `safety.EnsureHookDeployed(projectDir)` writes the embedded hook script to `.claude/hooks/claude-safety-hook.sh` with mode `0555` (read+execute, no write). 2. It merges a `PreToolUse` entry into `.claude/settings.json` without touching other keys (e.g., an existing `security-gate.sh` from CLAUDE.md is preserved). 3. Claude Code evaluates `PreToolUse` hooks matching `Bash|Edit|Write|MultiEdit|NotebookEdit`, pipes the tool-call JSON to the hook on stdin, and honours its exit code (`0` allow, `2` block). 4. Only then does the dispatcher send `claude --dangerously-skip-permissions`. The hook script is **embedded in the Go binary** (`//go:embed hook.sh`) so there is a single source of truth. Every launch overwrites the deployed copy, so a stale or tampered hook is self-healing on the next session. ## What the hook blocks **Bash patterns (exit 2, message shown to the model):** - `rm -rf /`, `rm -rf /*`, `rm -rf ~`, `rm -rf $HOME`, `rm -rf /home`, `rm -rf /etc`, `rm -rf /usr`, `rm -rf /var`, `rm -rf /boot` - `dd of=/dev/{sd,nvme,disk,hd,mmcblk}*` and shell redirects to the same - `mkfs` / `mkfs.*` - `git push --force` / `git push -f` (but `--force-with-lease` is allowed — it is safe in rebased workflows) - `git reset --hard` when the target is `main`, `master`, or `production` - `git clean -fdx /`, `git clean -fdx ~`, `git clean -fdx $HOME` - `sudo …` except the short allow-list `systemctl`, `journalctl`, `cp`, `install` - `su -`, `su root` - `chmod 777` on `/`, `/etc`, `/usr`, `/var`, `/boot`, `~`, `$HOME` - `curl … | sh|bash|zsh|dash`, `wget … | sh`, `bash <(curl …)`, `sh <(wget …)` - `eval "$(curl …)"`, `eval $(wget …)` - `crontab -e`, `crontab -E`, `>> /etc/crontab` - Canonical fork bomb `:(){ :|:& };:` **Bash patterns (warn only — logged, not blocked):** - `kubectl delete`, `helm uninstall`, `terraform destroy` - `DROP TABLE`, `TRUNCATE TABLE`, `DELETE FROM … WHERE 1=1` **Edit/Write/MultiEdit/NotebookEdit paths (exit 2):** - Anything under `.claude/settings*.json`, `.claude/hooks/` (project and `~/.claude/`) — anti self-disable - `~/.ssh/authorized_keys`, `~/.ssh/config` - `~/.bashrc`, `~/.bash_profile`, `~/.zshrc`, `~/.profile` - `/etc/sudoers`, `/etc/sudoers.d/*`, `/etc/systemd/*`, `/etc/crontab`, `/etc/cron.*/*` ## What the hook does **not** do - It does not gate MCP tools, `WebFetch`, or custom tool names not in the matcher list. Extend the matcher if a new high-risk tool is added. - It does not claim to be exhaustive against a determined attacker — it is a seat-belt for the pattern-based attacks enumerated in FNDG-04. Defense in depth still requires (a) MCP input sanitization, (b) rate-limiting on `sessions.send`, and eventually (c) Option C (two pools). ## Dependencies - `bash` 4+ (for `[[ … =~ … ]]` and `case`) - `jq` (used to parse the tool-call JSON). If `jq` is missing, the hook **fails closed** — every tool call is blocked and the log records `FAIL-CLOSED jq not installed`. ## Logs Each decision is appended to `$CLAUDE_SAFETY_HOOK_LOG` (default `~/.claude/safety-hook.log`). The log is per-user, not per-project. ## Deployment sites in claude-failover | Go file | Line | Context | |---|---|---| | `internal/dispatcher/dispatcher.go` | `launchAgent` | Every initial session launch. Failure to deploy is fatal — the agent will not start. | | `internal/switcher/account_switcher.go` | `relaunchDedicatedSessions` | After a quota failover resume. Failure is logged, not fatal — the pre-existing deployed hook remains in place. | ## Coordination with the bash dispatcher (`lib-common.sh`) The legacy bash dispatcher in `dev-management/agent-orchestrator/lib-common.sh` also launches `claude --dangerously-skip-permissions`. To stay consistent it should source the hook before spawning the session. Suggested integration: ```bash # In lib-common.sh, just before `tmux send-keys … "claude --dangerously-skip-permissions"`: go run forge.secuaas.ovh/olivier/claude-failover/cmd/deploy-safety-hook -- "$project_dir" \ || { echo "safety hook deploy failed, refusing to launch" >&2; exit 1; } ``` (Or vendor `hook.sh` + `settings.json` merge into a small shell helper — the Go package is the single source of truth either way.) Cross-repo wiring is tracked in a follow-up task; this PR only lands the claude-failover side. ## Testing ```bash go test ./internal/safety/ -v ``` Covers: - File layout and modes (hook script is `0555`, settings is valid JSON) - Idempotency (three deploys = one hook entry) - Existing settings keys are preserved - Stale matcher is refreshed - A representative set of malicious inputs exits `2` - A representative set of benign inputs exits `0` Manual test: ```bash echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~"}}' \ | bash .claude/hooks/claude-safety-hook.sh echo $? # → 2 ``` ## References - FNDG-04 audit: `secuaas-mcp` branch `audit/mcp-stdio-2026-04-18`, file `docs/audit-mcp-stdio-2026-04-18.md` - Decision: Olivier, 2026-04-19 — Option A now, Option C tracked separately - Task spec: `.agent-queue/inbox/20260418-211102-fndg-04b-dangerously-skip-permissions-sessions-headless.md`