Skip to content

feat(config-guard): block tampering with guardrail configuration - #39

Merged
karanb192 merged 3 commits into
mainfrom
feat/config-guard
Aug 18, 2026
Merged

feat(config-guard): block tampering with guardrail configuration#39
karanb192 merged 3 commits into
mainfrom
feat/config-guard

Conversation

@karanb192

@karanb192 karanb192 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Motivation

Every hook in this repo assumes its own config stays intact. Two real incidents broke that assumption:

  • The Aug 2026 CHAINDROP npm worm hid its payload in .claude/settings.json, using the agent's own config as the persistence mechanism.
  • CVE-2026-25725 let sandboxed code gain persistence by injecting a hooks entry into a settings.json that did not exist yet.

An agent (or anything riding on it) that can rewrite hook config can switch every other guardrail off. This PR closes that loop: who guards the guards.

Design decision

Checked the live hooks reference at code.claude.com/docs/en/hooks before writing anything. Two candidate surfaces:

  • PreToolUse is the primary guard (hook-scripts/pre-tool-use/config-guard.js): it fires before damage and supports deny/ask, so the agent's own Edit/MultiEdit/Write/Bash mutations of guardrail config are stopped up front.
  • ConfigChange is the secondary detector (hook-scripts/config-change/config-watch.js): the docs confirm ConfigChange CAN block (exit 2 stops the change from taking effect, except policy_settings), so the secondary is more than detect-and-warn. But the docs do not document the event's input payload schema, so the hook parses defensively (source/config_source/matcher, file_path/path), logs the raw payload for discovery, defaults to a visible systemMessage warn (ConfigChange also fires for changes you make yourself), and offers opt-in hard blocking via CONFIG_WATCH_BLOCK=true. The README section states these limits plainly.

What config-guard covers

  • Tiers follow the repo's SAFETY_LEVEL convention:
    • critical: .claude/settings.json, .claude/settings.local.json (any depth, so ~/.claude/ too), managed-settings.json, anything under .claude/hooks/ (the guard protects itself), hooks.json manifests
    • high: .mcp.json, .claude-plugin/, plus the CLI writes that rewrite agent config without naming a path: claude config set|add|remove, claude mcp add|remove, and claude plugin install|uninstall|enable|disable|update|marketplace add|remove (a plugin install registers arbitrary hooks); list/get forms stay allowed
    • strict (opt-in): CLAUDE.md, CLAUDE.local.md, .claude/rules/, .claude/agents/, .claude/commands/
  • Bash mutation forms caught: redirect (>, >>, >|) into a protected path, sed -i/perl -i/gawk -i inplace, tee/truncate, mv/cp/ln/rsync/install, rm/unlink/shred/git rm - all case-insensitive, since APFS/NTFS resolve RM and SED to the same binaries
  • Edit/MultiEdit/Write paths are resolved through symlinks before checking, so ln -s ~/.claude /tmp/x followed by a Write to /tmp/x/settings.json is still caught; a not-yet-existing file resolves its parent directory, which keeps the CVE creation vector covered
  • Precision: reads never block (Read tool, cat/jq/grep/ls/diff, even cat settings.json > /tmp/copy), unrelated dotfiles and app-level settings.json/webhooks.json/React hooks/ dirs pass
  • Escape hatches match repo style: CONFIG_GUARD_ALLOW=true for an intentional human-approved edit, and the existing HOOK_ASK_CRITICAL/HIGH/STRICT ask mode so intentional edits can degrade to a prompt
  • Known limits documented in the header the same way protect-tests does: not a shell parser, verb+path co-occurrence blocks, interpreter one-liners and git restore of config paths are not caught, and shell strings (unlike file-tool paths) are not symlink-resolved

Tests

144 new tests (118 for config-guard, 26 for config-watch), hermetic per the repo's audit conventions (temp HOME, env scrubbing, real symlink fixtures). Suite went from 1238 to 1382, zero failures; the meta discovery guard sees both new files.

ℹ tests 1382
ℹ pass 1382
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0

README: one row in the Pre-Tool-Use table, one new Config-Change section (motivation links + combined settings snippet), config-guard added to the ask-mode list, tests badge bumped.

- claude plugin install/uninstall/enable/disable/update and
  marketplace add/remove now count as config writes (a plugin
  install registers arbitrary hooks); list forms stay allowed
- Edit/MultiEdit/Write paths resolve through symlinks before
  checking, so ln -s ~/.claude /tmp/x then writing
  /tmp/x/settings.json is caught; nonexistent files resolve
  their parent dir (keeps the CVE vector covered)
- 9 new tests; badge synced to 1378
RM, SED -I, TEE, and CLAUDE MCP ADD resolve to the same binaries on
the case-insensitive filesystems macOS and Windows default to, so the
mutation-verb and CLI regexes now carry the i flag like the path
patterns already did. 4 new tests; badge synced to 1382.
@karanb192
karanb192 merged commit 5c995b4 into main Aug 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant