Skip to content

feat(content-guards): add secret-guard PreToolUse hook#338

Closed
JacobPEvans-personal wants to merge 1 commit into
mainfrom
feat/secret-guard-pretooluse-hook
Closed

feat(content-guards): add secret-guard PreToolUse hook#338
JacobPEvans-personal wants to merge 1 commit into
mainfrom
feat/secret-guard-pretooluse-hook

Conversation

@JacobPEvans-personal
Copy link
Copy Markdown
Member

Summary

Layer 1 of the multi-layer sensitive-value prevention system: a secret-guard
PreToolUse hook in the content-guards plugin that blocks hardcoded sensitive
homelab values from being written into a repo. It fires on
Write|Edit|MultiEdit|NotebookEdit and inspects the content field
(content, new_string, edits[].new_string, new_source).

Detection prongs

  • Literal — a private newline-separated POSIX-ERE denylist
    (SENSITIVE_DENYLIST) read from the macOS auto-readable keychain via
    security find-generic-password -s SENSITIVE_DENYLIST -w. Holds the exact
    real domain, node/pool names, and account ID. Never committed; referenced by
    name only.
  • Structural (value-free) — RFC1918 internal IP literals
    (10./192.168./172.16-31.), allowlisting fake test values (RFC2544
    198.18/198.19, example.invalid). A real-domain shape is supported but
    off by default (the hook fires on every write, so a broad TLD match would
    block ordinary content mentioning github.com etc. and be hostile) — opt in
    per repo via SECRET_GUARD_DOMAIN_REGEX. The real domain is caught by the
    literal prong instead.

Behavior

  • Deny protocol: mirrors webfetch-guard.py / main-branch-guard.py — JSON
    hookSpecificOutput/permissionDecision: deny on stdout, exit 0. The reason
    names the matched category, never the matched value, so a secret is not
    re-surfaced.
  • Fail-open: a missing/empty denylist, an unreadable keychain, or any
    internal error allows the write with a one-line stderr warning. Fresh clones
    and external contributors without the keychain entry are never blocked; the
    structural prong still runs when the literal denylist is unavailable.

Files

  • content-guards/scripts/secret-guard.py (new, executable)
  • content-guards/scripts/test_secret_guard.py (new, executable) — self-contained
  • tests/content-guards/secret-guard/secret-guard.bats (new) — CI bats suite
  • content-guards/hooks/hooks.json — register the PreToolUse matcher
  • content-guards/README.md, content-guards/ARCHITECTURE.md — docs
  • content-guards/.claude-plugin/plugin.json — version 1.7.0 -> 1.8.0, keywords

Testing (FAKE values only)

Tests use only fake values: RFC1918 IP shapes with non-homelab octets,
RFC2544 198.18/198.19 (allowlisted), example.invalid (allowlisted). CI
runs both the bats suite and the python test. Coverage: structural IP-shape
deny, RFC2544/example.invalid allowlisting, clean-content allow, Edit/MultiEdit/
NotebookEdit field extraction, domain prong off-by-default + opt-in, fail-open
on absent keychain, deny-reason omits the matched value, and (on macOS only) a
seeded TEST-keychain literal-denylist deny that cleans up its own entry.

Docs mirroring (for the lead)

Per repo-boundaries, mirror into JacobPEvans/docs
docs/ai-development/claude-code-plugins.mdx: content-guards now ships a
secret-guard PreToolUse hook (matcher Write|Edit|MultiEdit|NotebookEdit)
— keychain SENSITIVE_DENYLIST literal prong + value-free RFC1918 structural
prong, fail-open, with SECRET_GUARD_DOMAIN_REGEX opt-in. Plugin version 1.8.0.

🤖 Generated with Claude Code

Block hardcoded sensitive homelab values (real internal IPs, the real
domain, node/pool names, account IDs) from being written into a repo.
Fires on Write/Edit/MultiEdit/NotebookEdit and inspects the content field
(content, new_string, edits[].new_string, new_source).

Two detection prongs:
- Literal: a private newline-separated POSIX-ERE denylist (SENSITIVE_DENYLIST)
  read from the macOS auto-readable keychain. Never committed; referenced by
  name only.
- Structural (value-free): RFC1918 IP literals (10./192.168./172.16-31.),
  allowlisting fake test values (RFC2544 198.18/198.19, example.invalid). A
  real-domain shape is supported but OFF by default (fires on every write; a
  broad TLD match would be hostile) and opt-in via SECRET_GUARD_DOMAIN_REGEX.

Fail-open: missing/empty denylist, unreadable keychain, or any internal error
allows the write with a one-line stderr warning. The deny reason names the
matched category, never the matched value, so secrets are not re-surfaced.

Mirrors the established deny protocol (JSON hookSpecificOutput/deny on stdout,
exit 0) from webfetch-guard.py and main-branch-guard.py. Adds self-contained
python tests and a bats suite using FAKE values only; CI runs both.

Assisted-by: Claude:claude-opus-4-8[1m]
@JacobPEvans-personal
Copy link
Copy Markdown
Member Author

Closing per maintainer: NO custom scripts. The secret-guard PreToolUse hook was a bespoke Python script — rejected. Secret scanning will be gitleaks-only (no wrappers).

@JacobPEvans-personal JacobPEvans-personal deleted the feat/secret-guard-pretooluse-hook branch May 31, 2026 18:14
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