Mohamed Hegazy bc07f7a1fd
security-guidance: 5 precise if entries fixing #2089 regression + gt support
URGENT REGRESSION FIX. PR #2076 (Graphite gt workflow) gated the
PostToolUse commit/push hooks with:

    "if": "Bash(git commit:*)|Bash(gt create:*)|Bash(gt modify:*)"
    "if": "Bash(git push:*)|Bash(gt submit:*)"

mirroring the regex-OR idiom that `matcher` uses
("Edit|Write|MultiEdit|NotebookEdit"). But `if` is NOT a regex —
it's a SINGLE permission-rule string. The CC harness's dispatch
filter parses the entire `if` value as one rule of shape
`ToolName(rule_content)` via:

    let firstParen = H.indexOf("(");
    let lastParen  = H.lastIndexOf(")");      // searches from END
    if (lastParen !== H.length - 1) return { toolName: H };
    let toolName    = H.slice(0, firstParen);
    let ruleContent = H.slice(firstParen + 1, lastParen);

Applied to the broken commit clause:
    toolName    = "Bash"
    ruleContent = "git commit:*)|Bash(gt create:*)|Bash(gt modify:*"

The garbled `ruleContent` never matches any real command, so the
hook never fires — for ANY workflow, not just gt. The plugin's
deepest review layer was dead in production for all users on builds
shipping PR #2076.

Fix shape: split into separate hook entries, each with its own
well-formed single-rule `if` clause. The Python hook self-routes
commit vs push via the bash-command regexes and dedups concurrent
spawns via `_claim_bash_hook_once`, so multiple entries firing the
same script is safe.

This commit:

1. hooks.json: 5 precise entries (one per command shape) instead of
   the broken |-joined 2-entry form. Restores the original commit/
   push behavior bit-for-bit (`Bash(git commit:*)` + `Bash(git push:*)`
   are unchanged from pre-#2076), and adds 3 separate entries for
   the Graphite commands (`Bash(gt create:*)`, `Bash(gt modify:*)`,
   `Bash(gt submit:*)`). No git behavior change.

   The earlier draft used the broader `Bash(git *)` + `Bash(gt *)`
   per the reporter's suggestion, but that has a real cost: every
   `git status` / `git log` / `git diff` would spawn the Python
   hook only to early-exit via the regex matcher. Precise per-command
   entries avoid the spawn overhead and match the pre-#2076 cost
   profile exactly.

2. security_reminder_hook.py: widen `_GIT_COMMIT_RE` to tolerate
   `git -C <path>` and `git -c k=v` global options between `git`
   and `commit` (mirrors `_GIT_PUSH_RE`'s long-standing tolerance).
   Without this, `git -C /repo commit` is silently dropped by the
   handler — reporter flagged this as the secondary finding.

Verified locally on macOS Python 3.13:

  - hooks.json valid JSON, 5 `if` clauses each parses to a single
    `{toolName: "Bash", ruleContent: "<command>:*"}` pair.
  - py_compile security_reminder_hook.py clean.
  - 9-case regex sanity: all 4 commit forms match (bare, -C path,
    -c k=v, gt create/modify); 3 non-commit forms reject (status,
    gt submit, gt log). Pre-fix would reject -C path form.
  - 7 new tests in test_2089_if_clause_validity.py + 2 updated tests
    in test_gt_graphite_workflow.py:
      * 12 sanity tests for a Python parser mirroring harness's BA(H)
        — pinned so a future refactor can't silently start accepting
        the broken form.
      * 2 hooks.json validity: every `if` clause parses as a single
        valid rule; at least one if-gated hook exists.
      * 1 post-fix structure: separate entries cover git AND gt.
      * 2 updated gt-coverage: SOME clause covers git, SOME clause
        covers gt (no longer requires both in the same |-joined
        clause, which was the broken shape).

    TDD-verified the test catches the bug: temporarily restored
    main's broken |-joined hooks.json, ran the new test, saw
    `test_every_if_clause_is_single_valid_rule` fail with a clear
    error explaining #2089's cause. Restored fix, test passes.

  - Full suite: 336/353 pass (17 unrelated failures from open PRs
    #2078 / #2086 not in this branch).

NOT verified end-to-end with a real CC instance triggering the hooks
on a git or gt commit. The static-shape tests catch the regression
class and the regex sanity tests pin the `git -C` tolerance, but
the asyncRewake feedback loop needs runtime verification.

Closes #2089. Restores the closes for #2048 that PR #2076 attempted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:22:20 -07:00
..
2026-05-26 14:06:52 -07:00

security-guidance

Security review for Claude-generated code. Three layers:

  1. Pattern warnings — instant regex-based reminders on Edit/Write for ~25 known-dangerous patterns (yaml.load, torch.load(weights_only=False), pickle.load on untrusted data, raw innerHTML, hardcoded secrets, etc.).
  2. LLM diff review — when Claude finishes a turn, the plugin sends the diff to a fast LLM call (Opus 4.7 by default) and feeds high-severity findings back to Claude so it can fix them before you see the response.
  3. Agentic commit review — on git commit, an SDK-driven reviewer reads related files (Read/Grep/Glob) to trace data flow across the codebase, catching multi-file vulnerabilities pattern matching misses (IDOR, auth bypass, cross-file SSRF).

Findings cover common web-vulnerability classes — injection, XSS, SSRF, hardcoded secrets, IDOR, auth bypass, unsafe deserialization, and path traversal among others.

Install

/plugin install security-guidance@claude-plugins-official

Marketplace ships enabled by default in Claude Code — no setup beyond having the CLI itself.

Prerequisites

  • Claude Code CLI ≥ v2.1.144
  • Python 3.8+ on PATH (python3, python, or py -3 — the plugin picks the first that works)
  • A working API path (subscription, API key, or 3P provider config)

Configuration

All configuration is via environment variables. None are required for default behavior.

Selecting a model

# 1P / gateway: a canonical model id
SECURITY_REVIEW_MODEL=claude-opus-4-7   # default

# Bedrock: use the inference-profile id
SECURITY_REVIEW_MODEL=us.anthropic.claude-opus-4-7

# Vertex: use the Vertex date-tag form
SECURITY_REVIEW_MODEL=claude-opus-4-7@20260218

SECURITY_REVIEW_MODEL controls the LLM diff review. SG_AGENTIC_MODEL (same syntax) controls the agentic commit reviewer; defaults to the same model.

Enabling/disabling layers

Variable Default What it does
SECURITY_GUIDANCE_DISABLE=1 unset Kill switch — disables the entire plugin
ENABLE_PATTERN_RULES=0 on Disable layer 1 (regex pattern warnings)
ENABLE_CODE_SECURITY_REVIEW=0 on Disable all LLM reviews (Stop hook + commit/push)
ENABLE_STOP_REVIEW=0 on Disable only the Stop-hook diff review, keeping commit/push reviews. Useful for multi-agent / shared-worktree setups where another agent can move HEAD between a worker's turns
ENABLE_COMMIT_REVIEW=0 on Disable layer 3 (agentic commit review)

Higher-recall mode

SG_DUAL_OR=on   # default off

Runs two parallel review calls and unions the findings. Catches a few percentage points more vulnerabilities in our testing, at roughly 2× the API cost per review. Most users don't need it.

Org-specific policies

Drop a claude-security-guidance.md in any of:

  • ~/.claude/claude-security-guidance.md — user-wide rules
  • <project>/.claude/claude-security-guidance.md — project rules, intended to be committed
  • <project>/.claude/claude-security-guidance.local.md — local overrides, intended to be .gitignore'd

All three are loaded and concatenated into the LLM diff review's prompt in the order user → project → project-local. If the combined size exceeds the 8 KB prompt budget, the tail is truncated, so user-wide rules are kept and project-local rules are dropped first. The agentic commit reviewer (layer 3) does not currently read this file. Example:

# Acme security rules

- All SELECTs against the `customers` or `orders` tables MUST go through `db.replica`,
  never `db.primary`. Primary is for writes only.
- Background jobs must not use the user-context auth token; they get
  service-account creds from `jobs.get_service_account()`.
- Calls to `requests.get(url)` with a user-controlled `url` need
  the SSRF-allowlist wrapper at `acme.net.safe_request`.

Built-in rules cover common web-vulnerability classes without it — claude-security-guidance.md is for things specific to your codebase that the model can't infer.

Privacy and data handling

The plugin sends data to a model endpoint to perform its reviews. Specifically, each Stop-hook diff review transmits the changed file paths, the diff hunks, and the relevant file contents in the diff; each agentic commit review additionally transmits any files the reviewer pulls in via Read/Grep/Glob while tracing data flow. Your claude-security-guidance.md contents (user, project, and local) are appended to the prompt on every review, so don't put secrets in it.

Where that data goes depends on your Claude Code configuration:

  • Default (Anthropic API / subscription): sent to api.anthropic.com and handled under Anthropic's Commercial Terms and Privacy Policy.
  • LLM gateway (ANTHROPIC_BASE_URL set): sent to your gateway URL instead. The gateway operator's terms apply.
  • 3rd-party providers (Bedrock / Vertex / Foundry / Mantle): sent to your configured provider endpoint. The provider's data-handling terms apply (e.g., AWS / GCP / Azure).

The plugin writes its own debug log to ~/.claude/security/log.txt (override with SECURITY_GUIDANCE_DEBUG_LOG). The log contains diffstate metadata and finding categories — no full file contents or model prompts — and rotates at 1 MB. Nothing is uploaded.

Limitations

This is a best-effort assistive tool, not a guarantee. Treat findings as suggestions, not as a substitute for human code review, SAST/DAST, dependency scanning, or pen-testing. The reviewer can miss vulnerabilities, produce false positives, and may behave differently across codebases, languages, and model versions. No warranty is provided — use is subject to Anthropic's Commercial Terms.

Troubleshooting

Plugin doesn't seem to fire — check that ~/.claude/claude-security-guidance.md (or hook activity) shows in debug logs. Run Claude Code with --debug-file /tmp/claude/debug.txt and grep for security_reminder_hook. The plugin also writes its own log to ~/.claude/security/log.txt.

Review never finds anything — verify your API path works. On 3P providers, check SECURITY_REVIEW_MODEL is set to a provider-specific id (not a bare claude-opus-4-7). On LLM gateways, check the gateway's logs for POST /v1/messages traffic from the plugin.

Too many false positives — drop SECURITY_REVIEW_MODEL to a cheaper model (claude-sonnet-4-6) and re-evaluate; if precision is the priority, stay on Opus 4.7.

Want to silence a specific finding — add a comment to the line explaining why it's safe; the LLM reviewer treats inline justifications as exclusions. For systemic exclusions, document them in your claude-security-guidance.md.

Reporting issues

Open an issue on the security-guidance plugin repo with:

  • The Claude Code CLI version (claude --version)
  • Provider setup (1P / Bedrock / Vertex / LLM gateway / etc.)
  • A minimal repro diff
  • The relevant section of ~/.claude/security/log.txt