-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The fix is on a different sink from the bug the PoC ships\n\nThe public PoC for CVE-2025-59536 is [tacdm/cve-2025-59536-poc](https://github.com/tacdm/cve-2025-59536-poc). It is six lines of `package.json`, four lines of `index.js`, and a `.claude/settings.json` that registers a `SessionStart` hook running a shell payload. Clone the repo, run `claude` inside it, and the shell command fires before the trust dialog asks you whether you trust the repo.\n\nThe advisory ([GHSA-4fgq-fpq9-mr3g](https://github.com/advisories/GHSA-ph6w-f82w-28w6)) titles the issue \"Command execution prior to Claude Code startup trust dialog.\" The fix version is 1.0.111. That much you can read off the NVD entry. What you can only see by diffing the minified `cli.js` is that the code 1.0.111 modifies isn't the code the PoC exercises.\n\n## What 1.0.111 actually changed\n\nIn 1.0.110, the `apiKeyHelper` getter looks like this (renamed for readability):\n\n```js\nlet apiKeyHelperCache = memoize(() => {\n let helper = (loadSettings() || {}).apiKeyHelper;\n if (!helper) return null;\n try {\n let value = runShell(helper)?.toString().trim();\n if (!value) throw new Error(\"apiKeyHelper did not return a valid value\");\n return value;\n } catch (e) { /* warn */ }\n});\n```\n\nIn 1.0.111, the same function gains a pre-check:\n\n```js\nlet apiKeyHelperCache = memoize(() => {\n let helper = loadApiKeyHelper();\n if (!helper) return null;\n if (apiKeyHelperFromProjectSettings()) {\n if (!projectTrustAccepted(true)) {\n let err = new Error(\"Security: apiKeyHelper executed before workspace trust is confirmed. If you see this message, post in #claude-code-feedback.\");\n captureException(\"apiKeyHelper invoked before trust check\", err);\n statsigEvent(\"tengu_apiKeyHelper_missing_trust\", {});\n }\n }\n try {\n let value = runShell(helper)?.toString().trim();\n if (!value) throw new Error(\"apiKeyHelper did not return a valid value\");\n return value;\n } catch (e) { /* warn */ }\n});\n```\n\nThe new branch fires Sentry and Statsig. It does not `return`. `runShell(helper)` still runs. The \"fix\" is a telemetry funnel for the failure mode. Anthropic learns when this happens, and the customer's shell still ran.\n\n## The same diff adds a different function that does gate\n\n1.0.111 also introduces a wrapper that, unlike the getter, actually bails:\n\n```js\nfunction maybeRunApiKeyHelper() {\n if (loadApiKeyHelper()) {\n if (apiKeyHelperFromProjectSettings()) {\n if (!projectTrustAccepted(true)) return;\n }\n }\n apiKeyHelperCache();\n}\n```\n\nAnd a sibling that checks for the presence of an API key without executing the helper at all (`skipRetrievingKeyFromApiKeyHelper: true`). So the patch author knew exactly how to write a gate. They wrote one for the *startup probe* that decides whether to call the helper. They did not put one in the helper itself. The getter, which the rest of the codebase calls when it actually wants the key, still runs untrusted shell, just with a Statsig event attached.\n\nThis is an interesting fix to publish under a CVE number. It hardens the call site that the patch author was watching. It leaves every other call site to the same getter exactly as it was.\n\n## The hook execution path is unchanged\n\nThe PoC doesn't touch `apiKeyHelper`. It exploits `hooks.SessionStart`. So the question for the patch is: did 1.0.111 also gate that path?\n\nThe hook event-name table is identical in both versions:\n\n```js\nlet hookEvents = [\"PreToolUse\",\"PostToolUse\",\"Notification\",\"UserPromptSubmit\",\n \"SessionStart\",\"SessionEnd\",\"Stop\",\"SubagentStop\",\"PreCompact\"];\n```\n\nThe hook loader, matcher, runner, and `SessionStart` generator are byte-identical between 1.0.110 and 1.0.111. Only minified identifier names differ. None of those functions call `projectTrustAccepted`. There are exactly four `projectTrustAccepted` call sites in the 1.0.111 bundle: the three around `apiKeyHelper` shown above, and one inside the trust dialog component itself. Zero are in the hook execution path.\n\nThe CVE describes \"command execution prior to the trust dialog.\" The PoC demonstrates that property using hooks. The patch removes that property for one specific helper invocation. The PoC still works on the patched version, because the path the PoC takes was never touched.\n\n## Claude Code reads the file before it asks about the file\n\nThe interactive startup sequence in 1.0.111, also unchanged from 1.0.110, is:\n\n```js\nlet initialMessages = await loadStartupContext(\"startup\"); // SessionStart fires here\nrender(\n \n , root);\n```\n\n`loadStartupContext(\"startup\")` is what runs `SessionStart` hooks. It is awaited before React renders anything. The \"Do you trust the files in this folder?\" component, which enumerates MCP servers, hooks, bash commands, and API key helpers as the things the user is being asked to trust, is mounted by the render call after the await. The dialog asks about decisions the runtime has already started executing.\n\n**The trust dialog is a UI question about a decision the runtime has already started executing.**\n\nThis is the trust-inversion shape that keeps appearing in AI coding agents: the file the agent reads to decide how to behave is also a file that can tell the agent to run a shell command, and the read happens before the human is asked whether they trust the file. Cursor shipped one of these. opencode shipped one of these. The same primitive (\"agent loads a config file that contains executable directives\") keeps producing the same CVE shape.\n\n## The prior CVE on this dialog (1.0.87) treated it as a copywriting problem\n\nGHSA-ph6w-f82w-28w6, a different advisory on the same trust dialog, fixed in 1.0.87, was titled \"Insufficient warning in workspace trust dialog.\" The fix added text to the dialog explaining that accepting trust would run hooks and MCP servers. The mechanism wasn't changed. The user was told more clearly what they were about to consent to.\n\nTwenty-four releases later, CVE-2025-59536 says the trust dialog has a different problem: the consent question is asked after the shell has already run. The 1.0.111 fix changes what one helper does when the consent question hasn't been answered yet. It does not change *when* the consent question is asked.\n\nA trust dialog has shipped two CVEs in two months, and both fixes have left the shape of the dialog (its position in the boot sequence, the surface area it gates) alone. This is the design-debt-driver pattern: a primitive that the codebase keeps patching at the leaf instead of at the root, because moving the root would require restructuring how the agent boots.\n\n## What the next CVE on this dialog will look like\n\nThe `hookEvents` array has nine entries. The 1.0.111 diff gates one helper that isn't in that array. The next researcher to look at this surface will pick one of the eight remaining hook events, or `apiKeyHelper` called from a code path other than the startup probe, or one of the MCP-server-spawn paths that the trust dialog component enumerates but the boot sequence doesn't gate. The PoC will be the same six files. The advisory will be assigned a new CVE number. The fix will add another Statsig event.\n\nThe pattern holds until somebody moves the trust dialog ahead of `loadStartupContext`. Nothing in the 1.0.111 diff suggests that is on the roadmap.\n\n***\n\nPoC referenced: [github.com/tacdm/cve-2025-59536-poc](https://github.com/tacdm/cve-2025-59536-poc). Patched version: `@anthropic-ai/claude-code@1.0.111`. The reproduction in the README works against that version if you use `SessionStart` hooks instead of `apiKeyHelper`.","closing_line":"The patch closes one sink. There are nine.","hook_md":"CVE-2025-59536 was assigned to Claude Code on October 3, 2025. The fix shipped in 1.0.111. If you diff the npm tarballs, the change is small: when the `apiKeyHelper` setting comes from a project file and the workspace trust dialog hasn't been accepted, the runtime now calls `Ck(\"apiKeyHelper invoked before trust check\", ...)` and `Y1(\"tengu_apiKeyHelper_missing_trust\", {})`. Then it executes the shell command anyway. The PoC the advisory links to doesn't use `apiKeyHelper`. It uses a `SessionStart` hook, a different sink in the same settings file, whose code path is byte-identical between 1.0.110 and 1.0.111.","post_id":322,"slug":"claude-code-cve-2025-59536-patched-the-helper-not-the-hooks","title":"CVE-2025-59536: Claude Code Patched the apiKeyHelper. The PoC Exploits the Hooks.","type":"initial","unreadable_sentence":"The trust dialog is a UI question about a decision the runtime has already started executing."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaiCSJgAKCRDeZjl4jgkQ JlZ/AQC/OJ693N+kTbUsd9vLZauGL6MQSO45npm7zRH6bd0mnQEAiP6tSatMQJ+D jAB2FxXfGWhRvZAsRaWfYU63JtRe8As= =6Pfl -----END PGP SIGNATURE-----