The junior pastes a chat. The agent asked for Bash(git status). A plugin line then says Denied. The tool result is empty. They file the ticket: “the tool never ran. always-ask blocked it.”
I stop the run there. A withheld plugin result is not the tool never ran. Claude Code’s public notes that named this field are blunt: when a mod denies a tool call after the tool ran, Claude and you now read that the tool ran and a plugin withheld its result. The tool executed. The result was held. The empty pane is not a skip. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.295] [Source: https://code.claude.com/docs/en/changelog]
I already refused to treat a UNC path read as a local allow in A UNC Path Read Is Not a Local Allow, a leftover allowed-tools rule as you still in plan mode in A Leftover Allowed-Tools Rule Is Not You Still in Plan Mode, and a repeated reply ID as a second approval in A Repeated Reply ID Is Not a Second Approval. This post is the same desk rule for a denied tool with no result. Print withheld versus never ran. Name who owns the Claude Code answers.
The question is not whether the pane stayed empty. The question is whether the named owner can still tell a withheld plugin result from a tool that never ran.

The ticket that looks like never-ran
Juniors treat a missing tool result the way they treat a skipped call. Yesterday git status printed in the pane. Today a mod says Denied and the pane is blank. They page the desk: “the sandbox blocked it” or “always-ask never let it start.”
Two jobs collide on that row.
- Keep PreToolUse and PostToolUse apart. Official hooks page: PreToolUse fires before a tool call executes and can block it. PostToolUse fires after the tool already ran. Exit code 2 on PostToolUse still reaches Claude even though the tool already ran.
updatedToolOutputon PostToolUse replaces the tool’s result. A deny after the call is a result problem, not a skip. [Source: https://code.claude.com/docs/en/hooks] - Keep the new sentence honest. Official changelog under the 2.1.295 heading, 8 October 2026: changed what Claude and you read when a mod denies a tool call after the tool ran — it now says the tool ran and a plugin withheld its result. That sentence is the field. It is not “the tool never ran.” It is not “deny is off.” [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.295] [Source: https://code.claude.com/docs/en/changelog]
If you only screenshot “Denied” plus an empty result, you will file never-ran. You will not file the withheld result.
I do not invent a fake overnight outage of every plugin. I use the public contract. The withheld result is the ticket, not a version pin in the title.
What Claude Code actually named
Read the 8 October 2026 GitHub release body, then the docs changelog under the same heading, then the hooks page that names PostToolUse. Do not trust a social recap. Do not trust this post without those pages.
The named field, published 8 October 2026:
Changed what Claude and you read when a mod denies a tool call after the tool ran: it now says the tool ran and a plugin withheld its result
That sentence has three parts. Print all three on the ticket.
| Part | What it is | What it is not |
|---|---|---|
| Tool ran | The call executed (Bash started, Read opened, Edit wrote) | Proof the pane must show stdout |
| Plugin withheld the result | A mod denied after the run and hid what the tool returned | Proof the tool was skipped |
| Named owner | The human who answers Claude Code permission rows | A coding agent, a plugin, or auto mode |
Official hooks docs keep the timing loud. PreToolUse can block the call before it starts. PostToolUse cannot un-run it. A PostToolUse hook that exits 2 still tells Claude the stderr after the tool ran. updatedToolOutput replaces the inbound result. Redaction and transformation belong on PostToolUse for inbound results, not on a “never started” story. [Source: https://code.claude.com/docs/en/hooks]
The miss that looks like this ticket is the opposite mix: a junior sees Denied plus no stdout and files never-ran. After 8 October 2026, the product text names the mix. Print the new sentence. Do not file a skip.
Where the pin sits after the decision, not in the title
published_at 2026-10-08T19:48:38Z, not a prerelease). The docs changelog lists the same bullet under ## 2.1.295. npm dist-tags.latest on the morning this post shipped was 2.1.296; dist-tags.stable was 2.1.287; npm time for 2.1.295 was 2026-10-08T18:22:58.621Z. The field for this URL is the withheld-result line, not a pin in the title. Neighbor bullets in the same heading (OSC 7501, onFailure: "block", disableAutoMode leftover on Desktop/SDK) are other tickets. Do not mix nested deny, leftover allowed-tools, or UNC reads into this heading. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.295] [Source: https://registry.npmjs.org/@anthropic-ai/claude-code]Four checks before you file
A mid-senior screenshots this list. A junior who only has the chat still has a ticket they can close.
- Print whether the tool started. Look for a tool call id, a process start, a file open, or a Bash pid. If the log never shows a start, the ticket is “never ran” or “PreToolUse blocked.” If it shows a start, do not file never-ran.
- Print withheld versus empty versus skipped. Official release: the tool ran and a plugin withheld its result. Empty stdout after a run is not a skip. A PreToolUse block is a skip. Those are different tickets. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.295]
- Print PreToolUse versus PostToolUse. Official hooks page: PreToolUse is before execute. PostToolUse is after execute. Exit 2 on PostToolUse still reaches Claude after the tool ran. Do not collapse those two events into “Denied.” [Source: https://code.claude.com/docs/en/hooks]
- Write one human name.
CLAUDE_CODE_OWNER. That name answers “is this a withheld plugin result, or did we file never-ran on a tool that executed.”

Classify the row before you page the desk
Do not execute the original tool again to “prove” it. Classify the log line. The classifier below is a desk fixture. It never starts Bash. It never installs a plugin. It never hides a result.
1#!/usr/bin/env python3
2"""Classify withheld-result vs never-ran. Never re-run the tool."""
3from __future__ import annotations
4
5from typing import TypedDict
6
7
8class Ticket(TypedDict):
9 this_field: bool
10 ticket: str
11 reason: str
12
13
14def classify_row(
15 tool_started: bool,
16 event: str,
17 plugin_withheld: bool,
18) -> Ticket:
19 event_name = event.strip()
20 if plugin_withheld and tool_started:
21 return {
22 "this_field": True,
23 "ticket": "withheld_plugin_result",
24 "reason": "tool ran; plugin withheld the result",
25 }
26 if event_name == "PreToolUse" and not tool_started:
27 return {
28 "this_field": False,
29 "ticket": "pretooluse_block",
30 "reason": "call never started",
31 }
32 if not tool_started:
33 return {
34 "this_field": False,
35 "ticket": "never_ran",
36 "reason": "no start in the log",
37 }
38 return {
39 "this_field": False,
40 "ticket": "ran_with_visible_result",
41 "reason": "tool ran and the pane still has a result",
42 }
43
44
45FIXTURES = [
46 {"tool_started": True, "event": "PostToolUse", "plugin_withheld": True},
47 {"tool_started": False, "event": "PreToolUse", "plugin_withheld": False},
48 {"tool_started": True, "event": "PostToolUse", "plugin_withheld": False},
49]
50
51
52def main() -> int:
53 for row in FIXTURES:
54 out = classify_row(**row)
55 print(
56 f"this_field={out['this_field']} ticket={out['ticket']} {out['reason']}"
57 )
58 return 0
59
60
61if __name__ == "__main__":
62 raise SystemExit(main())
Run that against fixtures, not against production. The first fixture is this post. The second is a PreToolUse skip. The third is a normal PostToolUse with a visible result.
Print the ticket next to the owner. Do not let the agent invent a fourth class called “deny is off.”
Print the three lines on every withheld row
The printer is the artifact. A screenshot of Denied is not.
1#!/usr/bin/env python3
2"""Print withheld vs never-ran vs owner. Never hide a result."""
3from __future__ import annotations
4
5import os
6from classify_withheld import classify_row
7
8
9def print_ticket(
10 tool_name: str,
11 tool_started: bool,
12 event: str,
13 plugin_withheld: bool,
14) -> None:
15 owner = os.environ.get("CLAUDE_CODE_OWNER", "").strip()
16 if not owner:
17 raise SystemExit("CLAUDE_CODE_OWNER is empty")
18 row = classify_row(tool_started, event, plugin_withheld)
19 print(f"owner={owner}")
20 print(f"tool={tool_name}")
21 print(f"started={tool_started} event={event} withheld={plugin_withheld}")
22 print(f"this_field={row['this_field']} ticket={row['ticket']}")
23 print(row["reason"])
24
25
26if __name__ == "__main__":
27 print_ticket(
28 tool_name="Bash",
29 tool_started=True,
30 event="PostToolUse",
31 plugin_withheld=True,
32 )
If CLAUDE_CODE_OWNER is empty, stop. A coding agent does not fill that name. A Slack channel does not fill that name.
Official permissions page still evaluates deny before ask before allow. That order does not turn a PostToolUse withhold into a skip. Print the event name. Split “PreToolUse blocked” onto its own ticket. [Source: https://code.claude.com/docs/en/permissions] [Source: https://code.claude.com/docs/en/hooks]

Scan the log for withheld, not for “empty”
An empty pane is not a classifier. Scan for the product sentence, a PostToolUse event, or a plugin-withheld flag. Do not re-run the tool to fill the pane.
1#!/usr/bin/env python3
2"""Scan an agent log for withheld-result rows. Never execute the tool."""
3from __future__ import annotations
4
5import re
6import sys
7from pathlib import Path
8
9WITHHELD_RE = re.compile(
10 r"tool ran and a plugin withheld its result|plugin withheld its result",
11 re.I,
12)
13NEVER_RAN_RE = re.compile(r"the tool never ran|tool never started", re.I)
14POST_RE = re.compile(r"PostToolUse", re.I)
15
16
17def main() -> int:
18 path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("agent.log")
19 text = path.read_text(encoding="utf-8", errors="replace")
20 withheld = 0
21 never = 0
22 post = 0
23 for i, line in enumerate(text.splitlines(), 1):
24 if WITHHELD_RE.search(line):
25 print(f"{path}:{i}:this_field=True {line[:120]}")
26 withheld += 1
27 if NEVER_RAN_RE.search(line):
28 print(f"{path}:{i}:junior_ticket=never_ran {line[:120]}")
29 never += 1
30 if POST_RE.search(line):
31 post += 1
32 print(f"withheld={withheld} never_ran_claims={never} posttooluse={post}")
33 return 0
34
35
36if __name__ == "__main__":
37 raise SystemExit(main())
If the scan prints this_field=True, the ticket is this post. If it only prints PreToolUse blocks, look at nested deny, leftover allowed-tools, or UNC reads — those are other URLs. If it prints nothing, file a missing owner, a missing log, or a different permission field.
Official hooks docs still name the neighbor you must not collapse into this heading. updatedToolOutput replaces the inbound result after the tool ran. That is a withhold or a rewrite, not a skip. Do not write a recipe that strips PostToolUse so the pane always shows secrets. Print the event. Split “we need a visible redacted result” onto its own ticket. [Source: https://code.claude.com/docs/en/hooks]
Neighbor fields that are not this ticket
Keep the heading small. Neighbor fires from the same week are other URLs.
| Neighbor | What it is | Why it is not this post |
|---|---|---|
| Nested deny | A nested deny treated as a mod approval | Policy tree versus result pane, already shipped |
Leftover allowed-tools | A skill grant that survived into a later turn | Grant lifetime versus withheld result |
| UNC path read | A network-shaped Read after a local allow | Path spelling versus result pane |
| Repeated reply ID | A phone yes that reused an ID | Channel verdict versus withheld result |
| disableAutoMode leftover | Desktop/SDK staying off auto until restart after the setting was removed | Mode switch, leftover social, not this slug |
$.process.spawn withheld | Another mod denies a spawn after the child ran | Same product sentence, different tool, not this heading |
Do not steal those as a second heading. Do not clone A Nested Deny Is Not a Mod Approval. GSC this week still has no striking-distance query that asks for another nested-deny refresh. This unused URL is the withheld plugin result.

What you must not do
Forbidden:
- File “the tool never ran” or “always-ask blocked it” without printing whether the tool started, withheld versus skipped, PreToolUse versus PostToolUse, and one human name.
- Put
2.1.295,2.1.296, or8 October 2026in the title as a version-style hook. The numbers are evidence after the decision. - Write a plugin-bypass or hide-result recipe: stripping PostToolUse, forging a visible secret result, disabling mods so
.envalways prints, or teaching the agent to ignore a withhold. - Recommend buying a plan, a 20X badge, or a seat because a pane was empty. Official hooks page lists PreToolUse and PostToolUse as timing, not a purchase ticket. [Source: https://code.claude.com/docs/en/hooks]
- Treat nested deny, leftover
allowed-tools, UNC reads, repeated reply ID, disableAutoMode leftover, or$.process.spawnwithheld as this field. - Mix this field with allowed-mail,
ghs_length, lockfile, redirectedrm, or a Copilot assisted-permissions heading. Do not clone those shipped URLs. - Re-run the original tool against production to “fill the pane” as a demo.
- Let a coding agent own
CLAUDE_CODE_OWNER, or collapse a withheld result and a PreToolUse skip into one ticket.
Allowed:
- Print whether the tool started. Redact secrets if a log leaked a result you cannot store.
- Classify synthetic fixtures with
classify_withheld.py. Do not execute them against live tools. - Scan logs for “plugin withheld its result” and for junior “never ran” claims.
- Name one human as
CLAUDE_CODE_OWNER. - Keep withheld results, PreToolUse blocks, and nested deny as separate tickets.
- After the owner prints the four lines, treat Denied-plus-empty after a start as this field. Official release: the tool ran and a plugin withheld its result. Official hooks docs: PostToolUse is after execute. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.295] [Source: https://code.claude.com/docs/en/hooks]
- Put one line in the agent instructions file you already own: when a plugin denies after a tool ran, print withheld versus never ran and refuse to file never-ran. Check the printed ticket. Do not trust the agent to follow the line.
If you need the broader habit, start at /ai-agent-operations/. Tooling notes live under /developer-tools/. Laravel plus Vue notes live under /laravel-vue-saas/. A first-week map is at /start-here/.
A changelog bullet about withheld results is not permission to skip the four lines.
What you should do Monday morning
- Open the repo that actually runs Claude Code. Export
CLAUDE_CODE_OWNERto a human name. Run the ticket printer against yesterday’s agent log. Writethis_field=TrueorFalseon the ticket next to that name. - For every
this_field=Truerow, answer in one sentence: the tool started and a plugin withheld the result, or the call never started, or the pane still has a result, or the log never printed an event name. If you cannot answer, the ticket is “owner missing,” not “the tool never ran.” - Print PreToolUse versus PostToolUse. If the event is PreToolUse and the tool never started, that is a skip ticket. If the event is PostToolUse and a plugin withheld the result, official release notes name that field. Split those tickets. [Source: https://code.claude.com/docs/en/hooks] [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.295]
- Confirm coding-agent instructions on this desk name the same owner and forbid “the tool never ran” without the four lines. Forbid re-running production tools to fill a pane. Forbid writing a hide path that turns a withheld result into a visible secret dump.
- Confirm PostToolUse still uses the documented shape: the tool already ran, exit 2 still reaches Claude,
updatedToolOutputreplaces the inbound result. Do not invent a dashboard that mints “never ran” from an empty pane. Do not display a fake skip screenshot as proof. [Source: https://code.claude.com/docs/en/hooks] - Leave nested deny, leftover
allowed-tools, UNC reads, repeated reply ID, allowed-mail,ghs_length, lockfile, redirectedrm, disableAutoMode leftover, and Copilot assisted-permissions off this ticket. Those are neighbor fields. Do not steal them as a second heading.
The question is not whether the changelog demos well. The question is whether the named owner can still tell a withheld plugin result from a tool that never ran after handoff.
Further reading
Source GitHub — Claude Code v2.1.295 release notes
Source Claude Code Docs — Changelog
Source Claude Code Docs — Hooks (PostToolUse is after the tool ran)
