From 4c2226ae579dc390480f7ab79453690a8bc222ac Mon Sep 17 00:00:00 2001 From: Flo Date: Fri, 27 Mar 2026 21:11:26 +0100 Subject: [PATCH] Address spec review iteration 4: config keys, match targets, launchd Important fixes: - Document config key resolution (allowed_executables -> executables.allowed, mcp_allowed_servers -> names from [[mcp.servers]]) - Clarify CLAUDE_PROJECT_DIR source (derived from payload cwd field) - MCP rules: regex match_any operates on serialized tool_input, not tool_name - Add with_args_matching semantics (joined argument string) Suggestions also addressed: - Fix launchd plist: use absolute paths, note install.sh expands placeholders - Fix launchd socket path: use $TMPDIR for per-user isolation - Rename SECURITY_HOOKS_CONFIG -> SECURITY_HOOKS_HOME (contains both rules/ and config/ subdirectories) - Document directory discovery via single env var Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-26-security-hooks-design.md | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/superpowers/specs/2026-03-26-security-hooks-design.md b/docs/superpowers/specs/2026-03-26-security-hooks-design.md index 394abf2..860141b 100644 --- a/docs/superpowers/specs/2026-03-26-security-hooks-design.md +++ b/docs/superpowers/specs/2026-03-26-security-hooks-design.md @@ -57,6 +57,8 @@ Two kinds: ## Directory Structure +When installed, the entire tree below is copied to `$SECURITY_HOOKS_HOME` (default: `~/.config/security-hooks/`). The daemon discovers both `rules/` and `config/` as subdirectories of `$SECURITY_HOOKS_HOME`. This single env var controls all path resolution. + ``` security-hooks/ ├── bin/ @@ -300,6 +302,18 @@ regex_pattern := **Note:** `only_when` / `except_when` conditions are deferred to a future version. +### Config key resolution + +Config keys in `match_*_not_in` clauses map to TOML paths as follows: +- `match_base_command_not_in allowed_executables` → reads `executables.allowed` (flat list of strings) +- `match_server_not_in mcp_allowed_servers` → extracts the `name` field from all `[[mcp.servers]]` entries + +This mapping is hardcoded in the rule engine. Custom config keys are not supported in v1. + +### `with_args_matching` semantics + +The `with_args_matching(regex)` modifier matches the regex against the joined argument string (space-separated) of the matched command node, excluding the command name and flags. For example, for `git push --force origin main`, the argument string is `push --force origin main`. + ### Tiers - **block** — hard deny via `permissionDecision: "deny"`. The nudge is sent as `additionalContext` so the agent can self-correct. @@ -309,11 +323,11 @@ regex_pattern := Each hook type matches against a specific field from the Claude Code JSON payload: -| Hook file | Payload field matched | Example value | -|-----------|----------------------|---------------| -| bash.rules | `tool_input.command` | `rm -rf /tmp/foo` | -| edit.rules | `tool_input.file_path` | `/home/user/project/src/main.rs` | -| mcp.rules | `tool_name` (parsed into server + tool) | `mcp__context7__query-docs` | +| Hook file | Default match target | Notes | +|-----------|---------------------|-------| +| bash.rules | `tool_input.command` | Regex matches raw string; AST functions match parsed tree | +| edit.rules | `tool_input.file_path` | Regex matches the file path string | +| mcp.rules | `tool_name` for server/tool identification; serialized `tool_input` (JSON string) for parameter inspection | `match_server_not_in` extracts server name from `tool_name`; regex `match`/`match_any` matches against JSON-serialized `tool_input` to detect injection patterns | For bash rules, `{base_command}` is extracted as the first whitespace-delimited token of `tool_input.command` after stripping leading environment variable assignments. For AST-matched rules, it is extracted from the parsed command node. @@ -441,7 +455,7 @@ suspicious "edit-dependency-manifest" nudge "Dependency fields changed in {file_path} — use the package manager CLI" ``` -Note: `CLAUDE_PROJECT_DIR` and `HOME` in patterns are expanded to their actual values at rule load time. +Note: `CLAUDE_PROJECT_DIR` and `HOME` in patterns are expanded to their actual values. `CLAUDE_PROJECT_DIR` is derived from the `cwd` field of each hook payload (the working directory of the Claude Code session). `HOME` is taken from the process environment. If `cwd` is unavailable, the `edit-outside-project` rule is skipped (not fail-closed, since blocking all edits would be overly disruptive). ### mcp.rules @@ -594,7 +608,7 @@ Type=simple ExecStart=%h/.local/bin/security-hookd Restart=on-failure RestartSec=1 -Environment=SECURITY_HOOKS_CONFIG=%h/.config/security-hooks +Environment=SECURITY_HOOKS_HOME=%h/.config/security-hooks [Install] WantedBy=default.target @@ -617,14 +631,14 @@ After install: `systemctl --user enable --now security-hookd.socket` com.security-hooks.daemon ProgramArguments - ~/.local/bin/security-hookd + /Users/USERNAME/.local/bin/security-hookd Sockets Listeners SockPathName - /tmp/security-hooks/sock + TMPDIR/security-hooks/sock SockPathMode 384 @@ -636,13 +650,15 @@ After install: `systemctl --user enable --now security-hookd.socket` EnvironmentVariables - SECURITY_HOOKS_CONFIG - ~/.config/security-hooks + SECURITY_HOOKS_HOME + /Users/USERNAME/.config/security-hooks ``` +Note: `USERNAME` and `TMPDIR` placeholders are expanded by `install.sh` at install time. launchd does not expand tilde or environment variables in plist values. + After install: `launchctl load ~/Library/LaunchAgents/com.security-hooks.daemon.plist` ### Fallback: shim-managed daemon