The Claude Code security-guidance plugin, pushed to its limits
In the last post I pointed Anthropic's claude-security plugin at this blog's codebase: 82 agents, 21 minutes, one real bug. That plugin runs on demand. It has a quieter sibling, security-guidance, that is supposed to run all the time: on every edit, every turn, every commit Claude makes. I had it installed since August. I assumed it was reviewing me. This post is about finding out what it actually did, and about pushing its configuration as far as the source code allows.
Everything below happened in one live session, one prompt per step, on a small Python webhook relay I built for the purpose. The repo is on GitHub: zakariahere/advancedclaudesecurity. The slides in this post are from the deck I built from the same notes.
Three hooks, none of them blocks
The plugin is not a scanner. It is four hooks in Claude Code's loop, and three of them review something:

The per-edit layer is a regex pass with no model call. The per-turn layer diffs the working tree against a baseline captured when you submit a prompt and sends the diff, plus your guidance file, to a separate Claude with a "find problems" prompt. The per-commit layer is agentic: it reads callers and sanitisers before it reports. None of them stops a write. Findings come back as instructions and the writing Claude is expected to act on them. The docs say so plainly, and say to add a blocking hook yourself if you need a guarantee.
The twist: two of the three had never run
The plugin writes one line per decision to ~/.claude/security/log.txt. Nothing else. I wrote a 100-line script, tools/sg_log.py, that turns that file into a table of ran / skipped and why, and ran it over a month of daily use:
per-turn skipped: no credentials 730
per-turn skipped: not a git repo 395
commit skipped: no credentials 85
review (model calls) 0

The gate is one line in hooks/llm.py:
HAS_API_CREDENTIALS = bool(ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN or _HAS_3P_PROVIDER_AT_LOAD)
Hooks inherit the shell environment and nothing else. A subscription login keeps its OAuth token in ~/.claude/.credentials.json, which no hook reads. I verified it with a throwaway hook that printed presence-only of both variables, from the desktop app and from a scrubbed-environment claude -p: unset in both. So on a subscription-only machine, which is mine, the two model-backed layers skip every single turn, and the only trace is a line in a file nobody opens.
The fix is a real decision, not a config tweak. ANTHROPIC_API_KEY in your shell makes the reviews run, and also switches the whole session to API billing instead of the subscription. A gateway via ANTHROPIC_BASE_URL plus ANTHROPIC_AUTH_TOKEN works too, as does a Bedrock or Vertex flag. I have none of those, so this post is about the layer that does run, and about what I could bolt on around it.
The setup: one directory, three levels of trust
Everything lives in .claude/, and the important skill is knowing which file reminds, which advises, and which refuses.
security-patterns.yamlreminds. Nine custom rules for the per-edit layer, scoped withpathsglobs: literal secrets,==on a signature, outbound HTTP anywhere exceptfetch.py, SQL built with an f-string, subprocess argv built by formatting, and a meta-rule that fires when you edit the rules file itself.claude-security-guidance.mdadvises. The threat model of the service in plain English. The plugin concatenates the user, project and local copies, caps the result at 8 KB, and puts it in the reviewer's user turn inside a block that says it may add checks but must not suppress findings. A repo cannot silence the reviewer by committing "ignore SQL injection".hooks/guard.pyrefuses. Eighty lines of stdlib Python onPreToolUse. Edits toauth.py,fetch.py, the guard itself and any GitHub workflow needRELAY_ALLOW_PROTECTED_EDITS=1in the shell; content matching five credential shapes is refused everywhere.
Two gotchas from the YAML file alone, both silent. Write regexes in single-quoted scalars: "\s" in double quotes is an invalid YAML escape and the plugin skips the whole file with one log line. And PyYAML has to be importable by whichever Python the hook picked, not your venv. JSON always works.
Step 2: one Write, five sins, six warnings
I asked Claude to write a file with a shell-interpolated subprocess call, pickle.loads, an f-string SQL query, a raw urlopen, and == on a signature. One Write, and the tool result came back with six warnings attached.

The difference in voice is the whole argument for custom rules. The built-in pickle reminder is a paragraph listing eight libraries and three alternatives. Mine are one sentence each and name the project's own fix: "call relay.fetch.forward()", "look it up in the JOBS dict". Zero model calls, zero cost, and the check ran after the file landed on disk, which is the design: PostToolUse, not PreToolUse.
Also the first false positive arrived within minutes. My logging rule matched the word "signature" inside a log message rather than as an argument. Regex has no semantics. I tightened it to argument position and, more usefully, wrote tools/sg_patterns_check.py, which imports the plugin's own patterns.py and extensibility.py and runs them offline against any file. Run your new rule over the existing tree before you trust it.
Steps 3 and 4: two kinds of silence
Then I added a second shell=True to the same file. Nothing came back. The log shows the engine matched again; the emit was suppressed, because warnings fire once per rule per file per session, tracked in a state file under ~/.claude/security/. The tenth shell=True in that file is as dangerous as the first and just as silent.
Then I wrote the same sins through a Bash heredoc. Also nothing, for a different reason: the per-edit layer is wired to Edit|Write|MultiEdit|NotebookEdit, and the Bash handler only looks for git commit and git push. No pattern check ran at all. My guard has the identical blind spot.

Step 5: the one layer that says no
I asked for an Edit on auth.py replacing hmac.compare_digest with ==. The Edit tool returned an error instead of a result, git diff stayed empty, and the plugin's hook never ran: a PreToolUse deny short-circuits the chain, so there is nothing to post-process.

That is the whole reason the guard is narrow: two protected paths and five high-confidence credential regexes. Anything fuzzier stays in the YAML where a miss costs a sentence of context, not a blocked edit. And it hot-loaded: the settings file did not exist when the session started, and the next Write was already refused. A repo you cd into can add hooks to your session without ceremony, which cuts both ways.
Step 6: a valid regex, silently dropped
Last, I added a rule shaped like (a+)+. It compiles. It never loads. The plugin screens custom regexes statically for catastrophic-backtracking shapes (nested quantifiers, (.*)*, overlapping alternation under repetition), and drops matches with one log line. The other nine rules keep working, so nothing looks broken.

Scorecard

I still think the plugin is worth installing. The regex layer is free, immediate, and with ten minutes of custom rules it speaks your codebase's language. The guidance file is a genuinely good place to keep a threat model, because the reviewer that reads it cannot be talked out of a finding by the repo it is reviewing. The guard hook is the pattern I would copy into any repo an agent touches.
But install it, then run something like sg_log.py before you trust it. The plugin is honest about every skip. It just only talks to a file.
Repo, tools, notes and the deck: github.com/zakariahere/advancedclaudesecurity. Official docs: security-guidance, hooks, claude-security.