security-guidance: agentic commit review body is replaced by the inner CLI's stderr warning
Summary
For a subscription (OAuth) user with no ANTHROPIC_API_KEY, the security-guidance
plugin's agentic commit/push review runs successfully and produces findings, but
the findings body never reaches the model. It is replaced by the inner claude CLI's
startup warning:
⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another auth source is set
and takes precedence over your claude.ai login · Unset it to load your organization's connectors
The rewakeSummary is still correct, so the notification looks like a real review —
it just carries no findings. Observed in a real session as four consecutive rewakes:
<summary>Commit security review found: control-regression in .claude/settings.json</summary>
---
Background security review of commit — address or acknowledge the findings below:
⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another auth source is set ...
⚠ (same line, x3 — one per SDK spawn in the agentic run)
Environment
- Claude Code 2.1.265 (VS Code extension native binary) / 2.1.266 (PATH install)
- security-guidance plugin 2.0.7 (claude-plugins-official)
- claude-agent-sdk (Python) 0.2.137
- macOS 26.6.2, arm64
- Auth: claude.ai OAuth login (subscription). No
ANTHROPIC_API_KEY, noapiKeyHelper,
no third-party provider env vars.
Root cause
Three independent behaviours combine:
- The OAuth token is forwarded into the SDK grandchild as an env var.
hooks/llm.py:1091-1135 (_agentic_spawn_env) deliberately forwards
ANTHROPIC_AUTH_TOKEN when no ANTHROPIC_API_KEY exists — otherwise the grandchild
has no credentials at all. This is correct and intentional.
- The grandchild classifies an env-supplied bearer token as API-key precedence.
Seeing ANTHROPIC_AUTH_TOKEN in its env, the inner CLI takes the
api_key_precedence path, disables claude.ai connectors, and prints the warning
above to stderr — even though the token is the user's own claude.ai OAuth token.
- **The SDK does not pipe the child's stderr unless a callback is registered, and the
hook's stderr is the model-visible body channel.**
claude_agent_sdk/_internal/transport/subprocess_cli.py:850-851:
``python``
# Pipe stderr only when the caller registered a callback.
stderr_dest = PIPE if self._options.stderr is not None else None
_arun() (hooks/llm.py:1281-1330) builds ClaudeAgentOptions without a
stderr= callback, so the grandchild's stderr is inherited straight from the hook
process. Per the plugin's own note at hooks/security_reminder_hook.py:239-244,
Claude Code uses the hook's stderr || stdout as the asyncRewake body. The
commit-review path emits its findings only via
hookSpecificOutput.additionalContext on stdout
(hooks/security_reminder_hook.py:1478-1487) and writes nothing to stderr.
Result: stderr is non-empty (the warning lines), stderr wins, the findings body is
discarded. The summary survives because it is parsed separately from the stdout JSON.
The sibling helper _call_claude_via_sdk does register a
stderr callback at hooks/llm.py:404 — _arun() looks like a simple omission.
Reproduction
Minimal — shows step 2 in isolation (no API call, read-only):
$ env -u ANTHROPIC_AUTH_TOKEN claude mcp list 2>&1 >/dev/null
# (no warning; claude.ai connectors connect fine)
$ ANTHROPIC_AUTH_TOKEN=dummy-not-a-real-token claude mcp list 2>&1 >/dev/null
⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another auth source is set
and takes precedence over your claude.ai login · Unset it to load your organization's connectors
End to end:
- Log in with a claude.ai subscription (
/login); ensureANTHROPIC_API_KEYis unset. - Enable the security-guidance plugin (agentic commit review is on by default).
- Make a commit that the reviewer will flag.
- The rewake notification arrives with a correct summary and a body consisting only of
the connector warning, repeated once per SDK spawn.
Impact
The agentic commit/push review path is effectively unusable for OAuth subscription users
— which is the default configuration. The user sees a security notification with a real
finding category in the title and no detail at all, so there is nothing to act on. The
Stop-hook review is unaffected: handle_stop_hook calls analyze_code_security directly
(hooks/security_reminder_hook.py:1988) and never spawns a grandchild CLI.
Workaround: SG_AGENTIC_COMMIT_REVIEW=0 falls back to the single-shot path
(hooks/llm.py:1061-1073), which makes no child process and delivers the body correctly,
at the cost of the agentic exploration and adjudication passes.
Suggested fixes
Primary (plugin, one line). Register a stderr callback in _arun()'sClaudeAgentOptions, mirroring hooks/llm.py:404, so the grandchild's stderr goes todebug_log instead of being inherited:
stderr=lambda line: debug_log(f"agentic child stderr: {line}"),
Secondary (Claude Code, defence in depth). Session-level advisory warnings written to
stderr are unsafe under --print / stream-json, because a plugin hook's stderr is the
model-visible asyncRewake body channel. Consider suppressing them in that mode, or
routing them to the stream instead of stderr.
Optional (classification). The warning is arguably a misclassification: the credential
in ANTHROPIC_AUTH_TOKEN here is the user's claude.ai OAuth token, but supplying it via
env makes it count as an external auth source that overrides the claude.ai login. Not the
cause of the lost body, but it is what makes the message confusing to users.
Related issues
- #87145 — the same credential path on Windows, in the opposite direction: there
ANTHROPIC_AUTH_TOKEN is not injected into hook subprocesses at all, so
HAS_API_CREDENTIALS is false and the LLM review never runs. On macOS it is
injected, which is what makes this report possible. The two together suggest the
hook-subprocess credential contract is worth documenting explicitly.
- #81057 — security-guidance fail-quiet behaviour (a failed LLM review reports "no
vulnerabilities found"). Same family of problem: a review that did not deliver its
result still looks like a successful one to the user.