PreToolUse additionalContext is ephemeral (single-turn); permissionDecision deny reason persists - findings and a working delivery pattern
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Environment: Claude Code 2.1.237 as embedded by the Claude desktop app's Cowork mode, Windows 11. Hooks declared via a plugin's hooks/hooks.json (the only route that registers in Cowork; user-settings hooks resolve to a per-session directory the app never populates).
Field testing a plugin that injects reference facts via PreToolUse additionalContext surfaced two behaviors that do not appear in the hooks documentation. Related feature requests: #15345, #15664.
1. additionalContext is ephemeral. The injected string is readable by the model in the turn the matched tool call happens, and is gone from the model's context afterward. Sessions asked in a later turn to quote or use the injected text cannot - reliably, across many trials in one day - while sessions asked in the same turn quote it byte-accurately. This produced confusing secondary behavior worth knowing about: sessions interrogated later about injected content they demonstrably received (verified by a hook-side log of per-fire random tokens) variously denied receiving it, or retracted true reports and "confessed" to fabricating content the hook log proved real. If ephemerality is intended, a docs note would save others this investigation; if not, this is a bug report.
2. A deny reason persists where additionalContext does not. Returning permissionDecision: "deny" with the payload in permissionDecisionReason lands the text in the transcript as the blocked call's result, where it remains quotable and usable many turns later, and arrives before the action executes. We now deliver reference facts this way deliberately: the hook denies the first matched call per tool family per session (a marker file dedupes), the deny reason carries the facts plus a "re-issue the call and it will proceed" line, and the model retries the identical call, which passes. This works reliably in production use. It is also clearly a workaround: guardrail machinery used as a courier, at the cost of one bounced call per family per session.
Repro sketch
- Plugin hook on a tool matcher returns
{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"<distinctive text plus a random token generated at fire time>"}}, logging the token hook-side. - Same turn: ask the session to quote what arrived - it reproduces the text and token exactly.
- Later turn: ask again - the session cannot reproduce any of it, and the token (which exists only in the hook log and the injection) is unrecoverable from the session.
- Switch the same payload to
permissionDecision:"deny"with the text inpermissionDecisionReason: the model retries the call, completes the task, and can quote the full text byte-accurately in later turns.
What Should Happen?
Either: additionalContext injected by a PreToolUse hook remains available to the model for the rest of the session (like tool results do), or the documentation states the per-turn lifetime of additionalContext for each hook event so plugin authors can design for it. A supported persistent injection option for PreToolUse would remove the need for the deny-channel workaround: the use case is just-in-time delivery of tool-specific reference knowledge at the moment of first use, without paying always-on context for it.
Error Messages/Logs
Hook-side log excerpt (script logs each fire with a random token generated at emit time; "fired" lines were additionalContext emissions, "deduped" lines are marker-suppressed repeats):
2026-08-23 21:10:54 | fired | session=1bc447a7 | tool=mcp__33760a92-...__blotato_list_schedules | agent=main | rule=blotato | bytes=2018 | elapsed_ms=138 | token=FIRE-F6USAJWUUT
2026-08-23 21:10:58 | fired | session=1bc447a7 | tool=mcp__workspace__bash | agent=main | rule=sandbox | bytes=2462 | elapsed_ms=129 | token=FIRE-MR2GHZNK87
2026-08-23 21:12:07 | deduped | session=1bc447a7 | tool=mcp__33760a92-...__blotato_list_posts | agent=main | rule=blotato
Sessions quoted these tokens byte-accurately when asked in the same turn as the fire, and could not reproduce them or any payload text in later turns.
Steps to Reproduce
- Install a plugin whose hooks/hooks.json declares a PreToolUse hook on a tool matcher (e.g. Bash), whose command script returns {"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"<distinctive text plus a random token generated at fire time>"}} and logs the token to a file outside the workspace.
- In a session, trigger a matched tool call, and in the same turn ask the session to quote the hook context that arrived. It reproduces the text and token exactly, matching the log.
- In a later turn of the same session, ask it to quote the same block again. It cannot reproduce any of it; the token, which exists only in the hook log and the injection, is unrecoverable from the session.
- Change the hook script to return {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"<the same text and token>"}} for the first matched call only (a marker file dedupes). Trigger the call: the model receives the reason as the blocked call's result, re-issues the identical call, which proceeds, and in later turns can quote the full text byte-accurately.
Claude Model
Not sure / Multiple models
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.237 (embedded by the Claude desktop app, Cowork mode)
Platform
Other
Operating System
Windows
Terminal/Shell
Other
Additional Information
Related open feature requests asking for PreToolUse additionalContext support: #15345, #15664 - this report adds that even where the injection reaches the model, its single-turn lifetime undermines the knowledge-delivery use case. Happy to share the full hook script, routing data, and logs on request.