[BUG] A PreToolUse hook can permanently deadlock a Cowork session with no in-session recovery, and a new chat reuses the wedged container

Status Open
Maintainer reply None cached
Activity 0 comments · opened Aug 10, 2026

Product: Claude Cowork (cloud sandbox) / Claude Code plugin hooks
Component: PreToolUse hook execution + session/container lifecycle
Severity: High — no in-session recovery exists; a fresh environment was required.

Summary

A PreToolUse command hook that returns {"decision":"block"} for all matched tools blocks
every subsequent tool call in the session — including the commands needed to remove the
state causing the block
. Sub-agents are blocked too, worktree isolation is unavailable, no
unscreened tool can delete a container file, and starting a new chat reuses the same
container
, so the leaked state survives. Only a genuinely fresh sandbox clears it.

The plugin whose hook triggered this had its own bug (a state file leaked on an error path,
and the budget check ran before command analysis so it blocked unconditionally). That is
fixed. This report is about the platform gap that made it unrecoverable, which is
independent of any plugin.

Why there is no escape hatch

Every recovery avenue is mediated by the same blocked surface:

  1. Bash / Write / Edit / MultiEdit / NotebookEdit are matched and blocked — so

rm-ing the trigger file or editing hooks.json is blocked.

  1. Sub-agents are blocked: the plugin registers a second PreToolUse entry matching

Task|Agent, so the dispatch call itself is refused. The session cannot delegate its own
recovery.

  1. Worktree isolation cannot help: the session home is not a git repository. Verified in

a clean container — git rev-parse --show-toplevel returns fatal: not a git repository
and /home/claude/.git does not exist. EnterWorktree needs a repo at cwd for the same
reason.

  1. No unscreened tool can delete a container file. The device-bridge tools reach only the

user's machine (and cannot unlink even there); project-memory tools write to a separate
store, not the container filesystem.

  1. A new chat reuses the container, so the on-disk trigger state persists and the hook

keeps blocking. This defeats the user's most natural recovery instinct.

Note the real matcher was broader than a five-tool list — it ended in a catch-all regex over
any write-ish verb (.*(write|edit|patch|delete|remove|rename|move|create|upload|execute|run|
shell|command).*
), plus the separate Task|Agent entry. A reproduction should use both.

Minimal reproduction (plugin-agnostic)

  1. In a Cowork cloud session, register a PreToolUse command hook matching

Write|Edit|MultiEdit|NotebookEdit|Bash and a second matching Task|Agent, whose
command unconditionally prints {"decision":"block","reason":"blocked"} and exits 0.

  1. Any tool call is blocked — expected.
  2. Try to recover: rm the trigger file, edit hooks.json, dispatch a sub-agent — all

blocked, because each is itself a matched tool call.

  1. Start a new chat in the same container — the hook config and trigger state persist, so

it is still blocked.

  1. Observe there is no in-session control to disable the hook, bypass it for one command, or

reset the container.

Expected: some supported path back. Actual: none short of a new environment.

Suggested fixes — any one would have prevented this

  1. A user-facing "disable plugin hooks" control that takes effect in the current session.
  2. A hook-bypass for control-plane actions — one command or file deletion exempt from

PreToolUse, behind explicit user confirmation. A blocked session should never be able to
block its own recovery.

  1. A "reset/recreate container" action surfaced when a session appears wedged.
  2. Do not reuse a container for a brand-new chat by default, or make the inheritance

explicit — the current behaviour defeats the obvious recovery attempt.

  1. A watchdog on hooks that block 100% of calls for N consecutive tries, prompting the

user to disable the plugin.

  1. A warning in the hook-contract docs that a PreToolUse hook able to block its own

recovery commands creates an unrecoverable state, with a recommendation to exempt control
actions — and specifically to think twice before matching Task|Agent.

Workaround

Abandon the container and start again in a fresh environment — for a Cowork user, a session
that provisions a new sandbox (for example running the task on your own computer via the
desktop app), since the leaked state does not exist there.

Happy to provide session/container identifiers on request.

View original on GitHub ↗