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.
CVE-2025-59536: Claude Code Patched the apiKeyHelper. The PoC Exploits the Hooks.
patterns
cve
proof of concept
The fix is on a different sink from the bug the PoC ships
The public PoC for CVE-2025-59536 is 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.
The advisory (GHSA-4fgq-fpq9-mr3g) 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.
What 1.0.111 actually changed
In 1.0.110, the apiKeyHelper getter looks like this (renamed for readability):
let apiKeyHelperCache = memoize(() => {
let helper = (loadSettings() || {}).apiKeyHelper;
if (!helper) return null;
try {
let value = runShell(helper)?.toString().trim();
if (!value) throw new Error("apiKeyHelper did not return a valid value");
return value;
} catch (e) { /* warn */ }
});In 1.0.111, the same function gains a pre-check:
let apiKeyHelperCache = memoize(() => {
let helper = loadApiKeyHelper();
if (!helper) return null;
if (apiKeyHelperFromProjectSettings()) {
if (!projectTrustAccepted(true)) {
let err = new Error("Security: apiKeyHelper executed before workspace trust is confirmed. If you see this message, post in #claude-code-feedback.");
captureException("apiKeyHelper invoked before trust check", err);
statsigEvent("tengu_apiKeyHelper_missing_trust", {});
}
}
try {
let value = runShell(helper)?.toString().trim();
if (!value) throw new Error("apiKeyHelper did not return a valid value");
return value;
} catch (e) { /* warn */ }
});The 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.
The same diff adds a different function that does gate
1.0.111 also introduces a wrapper that, unlike the getter, actually bails:
function maybeRunApiKeyHelper() {
if (loadApiKeyHelper()) {
if (apiKeyHelperFromProjectSettings()) {
if (!projectTrustAccepted(true)) return;
}
}
apiKeyHelperCache();
}And 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.
This 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.
The hook execution path is unchanged
The PoC doesn't touch apiKeyHelper. It exploits hooks.SessionStart. So the question for the patch is: did 1.0.111 also gate that path?
The hook event-name table is identical in both versions:
let hookEvents = ["PreToolUse","PostToolUse","Notification","UserPromptSubmit",
"SessionStart","SessionEnd","Stop","SubagentStop","PreCompact"];The 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.
The 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.
Claude Code reads the file before it asks about the file
The interactive startup sequence in 1.0.111, also unchanged from 1.0.110, is:
let initialMessages = await loadStartupContext("startup"); // SessionStart fires here
render(<App initialState={s} ...>
<Chat initialMessages={initialMessages} ... />
</App>, root);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.
The trust dialog is a UI question about a decision the runtime has already started executing.
This 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.
The prior CVE on this dialog (1.0.87) treated it as a copywriting problem
GHSA-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.
Twenty-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.
A 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.
What the next CVE on this dialog will look like
The 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.
The pattern holds until somebody moves the trust dialog ahead of loadStartupContext. Nothing in the 1.0.111 diff suggests that is on the roadmap.
PoC referenced: github.com/tacdm/cve-2025-59536-poc. Patched version: @anthropic-ai/[email protected]. The reproduction in the README works against that version if you use SessionStart hooks instead of apiKeyHelper.
The patch closes one sink. There are nine.