[Bug] security-guidance plugin reads unencrypted secrets into context, bypassing guardrails
Status Open
Reported on v2.1.280
Maintainer reply None cached
Activity 3 comments · opened Sep 23, 2026
Bug Description
security-guidance read secret files in to the context while I was editing them and before I was able to encrypt them.
Claude should never read secrets in clear into its context. I have several guardrails for that, however the plugin bypassed them all.
Environment Info
- Platform: darwin
- Terminal: Apple_Terminal
- Version: 2.1.280
- Feedback ID: a968c68d-fb33-4e2b-95e8-f19bcaad581c
3 Comments
I traced the current public implementation to narrow down which path needs fixing. The per-edit handler runs local pattern checks; the Stop handler reviews a Git diff, while the commit reviewer can use Read/Grep/Glob over the repository. These are different paths, so the report alone does not yet establish which one bypassed your guardrails.
For reference, the public commit reviewer sets
setting_sources=[]: review options. That is a lead to investigate, not confirmation that it caused this incident.Could you share the following using dummy values only, with no real secrets or private file contents?
permissions.denyrule, a hook, or an instruction. A sanitized rule using a dummy path is enough.This should distinguish direct diff collection from the separate review agent's file access before choosing a fix. No need to upload your original files. Source inspection prepared with AI assistance; I have not reproduced your reported incident.
Thanks for the pointers @7487. I reproduced it with a throwaway repository and a fake marker. Everything below uses dummy values only.
Environment
2.0.8fromclaude-plugins-official(commit2cd88e79), user scopeagentic_review: SDK unavailable (No module named 'pydantic_core._pydantic_core'); falling back), so the reviews below ran on the single-shot fallback path, not the agentic Read/Grep/Glob reviewer.Guardrail type
No
permissions.denyrule. The guardrail is a user-levelPreToolUse,PostToolUsehook matched onRead,WriteandBashthat blocks reading files containing secrets using keywords, Shannon entropy and heuristics, together with instructions not to read secrets. None of these get a chance to fire here: the secret never goes through a tool call in the main session. It arrives as Stop-hook feedback.Minimal repro
mk.shstands in for "the user is editing a plaintext secrets file in the working tree before encrypting it". The main agent only ever sees the command./mk.sh A2, never the file content.Where the marker appeared
| Location | Marker present? |
|---|---|
| Main conversation (transcript and stream-json), as a
Stophookhook_blocking_error/ "Stop hook feedback" user message | Yes:1. [HIGH] [Hardcoded Secrets] password: SG_FAKE_MARKER_A2_7f3a91|| Main agent's following turn (thinking and reply) | Yes, it repeats the value |
| Plugin debug log (
SECURITY_GUIDANCE_DEBUG_LOG) | No || Reviewer prompt | Not captured directly (fallback path). The debug log shows
Stop hook: review_set=1 … reviewing 1 changed files (standard diff), and the finding quotes the value, so the file content was in the review input. |Relevant debug log lines:
Observations
secrets.ymlmodified during the turn becomes part of the review set (.ymlis inSOURCE_CODE_EXTENSIONS). Its full content goes to the review model, and the finding text quotes the secret value back into the main conversation..envinstead of.ymlthere was no review at all (no source code files in diff)../mk.sh B && git add -A && git commit) ran the commit review and then the Stop review (both fallback). Neither reported a finding, so the marker did not appear in the main conversation in that run.Suggested fix direction
password: ****91) before building the Stop-hook feedback.*secret*,*.tfvars, SOPS-candidate YAML) from the diff sent to the reviewer, or redacting high-entropy values before sending.setting_sources=[], so a user'sPreToolUseRead guards would not apply to its Read/Grep/Glob either. I could not exercise that path here because the SDK import fails.Thanks for reducing this to a throwaway repo and dummy values. This clarifies that your observed Stop case uses custom tool hooks, not a
permissions.denyrule. The earliersetting_sources=[]concern is a separate agentic-review path. In the current public code, Stop is deliberately single-shot, so an SDK import failure is not required for this Stop path.I checked public main
d78be948and draft #96434 at3bd80115, using isolated Git repositories with untracked files and fake markers. I ran the realget_git_diff→parse_diff_into_files→analyze_code_securityfunctions, intercepting the model-call boundary; no real model or user hook was run. Results:| Case |
secrets.ymlmarker in review prompt |runtime-config.ymlmarker in review prompt ||---|---|---|
| Current main, no Read deny rules | yes | yes |
| #96434 defaults, no Read deny rules | no | yes |
| #96434 with
SG_SKIP_SECRET_FILES=0| yes | yes || #96434 defaults +
Read(./runtime-config.yml)deny | no | no |The
.envcontrol was excluded in all four cases. I also ran the PR's 14 unittest tests successfully.This means the PR's default secret-filename filter covers the
secrets.ymlinput in your repro, independently of permissions.deny. It does not replay arbitrary content-based user hooks, and the finding formatter still emitsvulnerableCode/fixwithout new redaction. A synthetic finding quoting a marker in the remaining ordinary-named config file still appears in the formatted feedback; that checks the formatting boundary, not whether a real model would produce that finding.For #96434, the no-deny-rule Stop case and this custom-hook/output-redaction boundary are worth keeping explicit in the tests and documentation. The PR is still a draft; I have not verified native CLI/model end-to-end behavior or the reporter's marketplace build. These checks were prepared with AI assistance.