feat(safety): PreToolUse hook gating destructive tool calls (FNDG-04b, Option A)
Adds internal/safety/ — the in-repo source of truth for the PreToolUse hook
deployed into every project before a Claude Code agent is launched. The hook
blocks destructive Bash/Edit/Write patterns on sessions running with
--dangerously-skip-permissions, closing the exploitation path where a prompt
injection via MCP sessions.send could otherwise trigger arbitrary destruction
without interactive confirmation.
Wire-up:
- internal/dispatcher/dispatcher.go launchAgent: deploys hook before claude
launch; fail-closed if deployment fails.
- internal/switcher/account_switcher.go relaunchDedicatedSessions: redeploys
hook before --resume after account failover; fail-open (log + continue)
since the initial deployment is still in place.
Blocks (exit 2, stderr shown to model):
- rm -rf targeting /, ~, $HOME, /etc, /var, /usr, /boot
- dd of=/dev/{sd,nvme,disk,hd,mmcblk}*, mkfs*
- git push --force (but allows --force-with-lease)
- git reset --hard on main|master|production
- sudo outside short allowlist (systemctl, journalctl, cp, install, apt*)
- curl|sh, bash <(curl ...), eval "$(curl ...)", fork bomb, crontab -e
- chmod 777 on system paths / home
- Writes to .claude/settings*.json, .claude/hooks/, ~/.ssh/authorized_keys,
shell rc files, /etc/sudoers*, /etc/systemd/*
Warn-only (logged, not blocked):
- kubectl delete, helm uninstall, terraform destroy
- DROP TABLE, TRUNCATE TABLE, DELETE FROM ... WHERE 1=1
Hook script is embedded via //go:embed so a single binary release carries
the authoritative copy. Every launch rewrites the deployed file with mode
0555 (anti-tamper); the hook itself also blocks writes to .claude/hooks/
for defense in depth.
Decision: Olivier, 2026-04-19 — Option A now, Option C (two pools) tracked
separately. Complements FNDG-04 input sanitization in secuaas-mcp.
Tests: 8 unit/integration tests in internal/safety/, plus a dispatcher-level
test verifying the hook is written before launch. go vet clean, go test ./...
all pass.
Refs: FNDG-04 audit (secuaas-mcp branch audit/mcp-stdio-2026-04-18)
Task: .agent-queue/inbox/20260418-211102-fndg-04b-*.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
336f1f27bb
commit
58690da69f
8 changed files with 885 additions and 1 deletions
|
|
@ -210,6 +210,47 @@ func TestDispatchProject(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestDispatchProjectDeploysSafetyHook verifies that the PreToolUse safety hook
|
||||
// is written to the project directory before Claude is launched (FNDG-04b).
|
||||
func TestDispatchProjectDeploysSafetyHook(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
inbox := filepath.Join(dir, ".agent-queue", "inbox")
|
||||
os.MkdirAll(inbox, 0755)
|
||||
|
||||
taskPath := filepath.Join(inbox, "task-hook.md")
|
||||
os.WriteFile(taskPath, []byte("---\npriority: high\n---\nDo the work."), 0644)
|
||||
|
||||
tc := newMockTmux()
|
||||
tc.sessions["pool-0"] = true
|
||||
tc.paneOutput["pool-0"] = "❯ "
|
||||
|
||||
s := state.New("")
|
||||
s.SetIdle("pool-0")
|
||||
|
||||
d := &Dispatcher{
|
||||
tmux: tc,
|
||||
state: s,
|
||||
config: &config.Config{
|
||||
Pool: config.PoolConfig{
|
||||
Autonomous: config.AutonomousConfig{Prefix: "pool-", Max: 1},
|
||||
},
|
||||
},
|
||||
logger: log.Default(),
|
||||
}
|
||||
|
||||
d.dispatchProject(inbox)
|
||||
|
||||
hookPath := filepath.Join(dir, ".claude", "hooks", "claude-safety-hook.sh")
|
||||
if _, err := os.Stat(hookPath); os.IsNotExist(err) {
|
||||
t.Errorf("safety hook not deployed at %s", hookPath)
|
||||
}
|
||||
|
||||
settingsPath := filepath.Join(dir, ".claude", "settings.json")
|
||||
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
|
||||
t.Errorf(".claude/settings.json not created at %s", settingsPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDispatchProjectNoFreeSession leaves the task untouched when no session is available.
|
||||
func TestDispatchProjectNoFreeSession(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue