v2.1.141 Fixed My 3 Claude Code Hook Bugs

A practical upgrade guide for senior devs already running Claude Code hooks in production. Walks through the three concrete fixes that landed in v2.1.139 to v2.1.141 (terminalSequence for headless notifications, args:string[] for…

Published Updated

v2.1.141 Fixed My 3 Claude Code Hook Bugs

Claude Code hooks let you block a risky tool call, check work before a turn ends, and record what the agent attempted. Since version 2.1.295, a broken command or HTTP policy hook can block an action instead of letting it proceed. Put prevention on PreToolUse, test feedback on Stop, and logging around tool execution.

The hook contract and release details below were verified against Anthropic's documentation and changelog on October 12, 2026. The examples are a starting configuration for a development repository, with their limits stated alongside them.

Claude Code hooks: events and hook types

An event decides when a check runs; a hook type decides how it runs. For a business application, the useful distinction is whether you must prevent an action, inspect its result, or ask the agent to keep working.

PreToolUse runs before execution, so it can refuse a destructive command. PostToolUse runs after success, so it can review the result but cannot undo a deleted file or a request already sent. Stop runs when the main agent finishes responding, which makes it useful for a final test check. Anthropic's hook lifecycle reference describes these boundaries.

Pick the handler for the job

Command hooks, type: "command", run a local program. They receive event JSON on standard input and communicate through an exit code or JSON output. Choose them for tests, linting, local policy checks, and logs.

HTTP hooks, type: "http", send that event JSON to an endpoint as a POST request. Choose them when a central service owns the policy, such as whether a deployment destination is approved. The service returns hook decision JSON; an HTTP error and a policy refusal are different outcomes.

Prompt hooks, type: "prompt", ask a model to judge a condition. A prompt can include $ARGUMENTS, which inserts the event input. The model returns an ok decision and a reason when refusing. Use this for a judgment such as whether the final response addresses the requested acceptance criteria. A prompt judge does not itself execute your test suite.

The current reference also supports agent hooks, which can inspect files with tools and remain experimental, and MCP tool hooks, which call a tool exposed by a configured Model Context Protocol server. Hook types are event-specific: for example, Stop supports prompt evaluation, while SessionStart does not. Hook types and supported events.

What each event is for

Use the event nearest the decision you need. A customer-data export needs a check before the export tool runs; an activity report needs observations after it finishes.

EventPractical purpose
SessionStartLoad context when a session starts or resumes.
SetupPrepare an environment through the documented initialization or maintenance modes.
UserPromptSubmitValidate or enrich an incoming prompt before processing.
UserPromptExpansionInspect a user command after it expands into prompt text.
PreToolUseInspect tool arguments and refuse an action before execution.
PermissionRequestSupply a decision when a tool needs permission.
PermissionDeniedReact to an auto-mode denial; retries have event-specific rules.
PostToolUseInspect a successful result or record completion.
PostToolUseFailureRecord a failed tool call or supply recovery context.
PostToolBatchReview a completed batch of parallel tool calls before the next model call.
NotificationDeliver an alert when Claude Code emits a notification.
MessageDisplayCustomize displayed assistant text as it streams.
SubagentStartAdd context when a delegated agent starts.
SubagentStopCheck a delegated agent's completion separately from the main agent.
TaskCreatedCheck a task as it is being created.
TaskCompletedCheck the transition to a completed task.
StopReview the main agent's response before it finishes the turn.
StopFailureObserve a turn ending because of an API error.
TeammateIdleCheck whether an agent-team teammate should keep working.
InstructionsLoadedObserve which instruction files entered context.
ConfigChangeInspect settings changes during a session.
CwdChangedReact when the current working directory changes.
DirectoryAddedReact when another working directory is added.
FileChangedWatch configured filenames, including edits outside Claude's file tools.
WorktreeCreateSupply custom creation of an isolated working tree.
WorktreeRemoveClean up a working tree created through the corresponding hook.
PreCompactAct before conversation context is compacted.
PostCompactLog the compacted summary or update external state afterward.
PreModelSwitchCheck a requested model switch before it takes effect.
PostModelSwitchReact after the session's model changes.
ElicitationHandle an MCP server's request for user input.
ElicitationResultInspect that response before it returns to the MCP server.
SessionEndPerform session-end cleanup or logging.

This map follows the current event reference. Each event has its own input and decision schema. A notification event cannot become a tool gate just because its script exits with an error.

The settings format that hooks expect

Put shared project hooks in .claude/settings.json. Use .claude/settings.local.json for local project choices and ~/.claude/settings.json for your user-wide configuration. Organization policy belongs in managed settings. Hook locations.

The nesting matters: hooks contains an event array; each entry is a matcher group; its inner hooks array contains typed handlers. A command placed directly beside the matcher is not the handler structure shown in the current reference.

For a command hook in exec form, keep both command and args:

JSON
{
  "type": "command",
  "command": "node",
  "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.mjs"]
}

command names the executable and args supplies its arguments. Claude Code substitutes documented path placeholders without passing them through a shell. Omitting args selects shell form, where pipes, redirection, and shell quoting apply. Do not assume arbitrary $CLAUDE_* strings inside args expand into event data; read the JSON input instead. Command hook fields.

A tool matcher such as Bash selects the tool name. "*" selects every occurrence of that event. Stop has no matcher filter. Matching handlers run in parallel, so their order in the file is not an execution sequence.

Three useful hooks in one project config

Start with shell-deletion protection, a test check, and a metadata audit. These examples assume macOS or Linux, Node.js and npm on PATH, and a single-package Git repository whose npm test command finishes without watch mode. In a monorepo, replace that test command with the repository's actual verification command.

Create .claude/hooks/ and save the following files. Merge this hooks object into your existing settings; retain any existing handlers you still need.

The shared settings.json

JSON
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.mjs"],
            "onFailure": "block"
          }
        ]
      },
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/audit.mjs"]
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/test-before-stop.mjs"]
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/audit.mjs"]
          }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/audit.mjs"]
          }
        ]
      }
    ]
  }
}

These are three behaviors, with the audit script registered before execution and after either outcome. The guard's onFailure setting requires Claude Code 2.1.295 or later. Failure-policy reference.

Block a risky shell deletion

Save this as .claude/hooks/guard.mjs:

JavaScript
import { readFileSync } from "node:fs";

JSON.parse(readFileSync(0, "utf8"));
console.log(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason:
      "Shell deletion is blocked here. Describe the cleanup for human review."
  }
}));

The if condition selects Bash calls containing a matching rm subcommand, including a command such as rm -rf build. When selected, the script refuses the call. It deliberately refuses matched rm operations generally, including ordinary file deletion; it does not attempt to classify which files are valuable.

This is useful when an agent maintains a repository containing client exports or generated deliverables that should receive human review before deletion. It is also deliberately conservative: the documented if filter can run the handler when it cannot confidently parse a command.

This filter is not a complete deletion security boundary. Other tools and programs can remove files. Keep hard restrictions in permissions and protect the filesystem or remote service itself. Anthropic explicitly describes if as best-effort filtering. Bash condition matching.

Run tests before Stop

Save this as .claude/hooks/test-before-stop.mjs:

JavaScript
import { readFileSync } from "node:fs";
import { execFileSync, spawnSync } from "node:child_process";

const input = JSON.parse(readFileSync(0, "utf8"));
if (input.stop_hook_active === true) process.exit(0);

let passed = false;
try {
  const root = execFileSync(
    "git", ["-C", input.cwd, "rev-parse", "--show-toplevel"],
    { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
  ).trim();
  const result = spawnSync("npm", ["test"], {
    cwd: root,
    stdio: ["ignore", "ignore", "ignore"]
  });
  passed = !result.error && result.status === 0;
} catch {
  passed = false;
}

if (!passed) {
  console.log(JSON.stringify({
    decision: "block",
    reason: "npm test did not pass or could not run. Run npm test, fix the failures, and report any unresolved errors before finishing."
  }));
}

The script runs the repository's tests when Claude first tries to finish. A failed test or launch problem returns a reason for Claude to continue. It keeps test output away from the hook's JSON channel and asks Claude to run the failing command to inspect the diagnostics.

The Git-root lookup uses the input's cwd, so entering a worktree does not accidentally test the initial checkout. ${CLAUDE_PROJECT_DIR} locates the hook script; it stays anchored to the project where the session started. Path and worktree behavior.

The stop_hook_active guard allows the subsequent stop instead of creating a persistent retry loop. That means this is bounded feedback, not a guarantee that tests passed. It also does not run on user interruption or replace StopFailure handling. Keep required merge or release checks in CI, the automated checks enforced by your repository or deployment system. Stop input and decisions.

Log every tool call that exposes hook events

Save this as .claude/hooks/audit.mjs:

JavaScript
import { readFileSync, mkdirSync, appendFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

const input = JSON.parse(readFileSync(0, "utf8"));
const directory = join(homedir(), ".claude");
mkdirSync(directory, { recursive: true });
appendFileSync(join(directory, "tool-audit.jsonl"), JSON.stringify({
  at: new Date().toISOString(),
  event: input.hook_event_name,
  session_id: input.session_id,
  tool_use_id: input.tool_use_id,
  tool_name: input.tool_name,
  agent_id: input.agent_id,
  cwd: input.cwd,
  duration_ms: input.duration_ms
}) + "\n", { mode: 0o600 });

The file contains one JSON record per observed event. Join records by session_id and tool_use_id to connect an attempt with its success or failure. This helps a technical lead investigate which tool activity preceded a broken build without recording raw prompts, file contents, or command arguments.

PreToolUse records an attempt, including one another hook rejects. PostToolUse records success; PostToolUseFailure records failure. A missing post-event does not prove success or identify why execution never happened. Anthropic also excludes EndConversation from the ordinary pre/post tool events. This is an activity log with those boundaries, not a complete or tamper-proof audit trail. Tool lifecycle and coverage.

The audit handlers retain the default failure behavior: a broken local log does not stop work. If recording an attempt is mandatory, an applicable synchronous PreToolUse audit hook can use onFailure: "block", backed by a protected logging service. Decide explicitly whether unavailable logging should make tool use unavailable too.

What onFailure: "block" lets a team enforce

Since October 8, 2026, Claude Code 2.1.295 lets command and HTTP hooks treat operational failure as a reason to block the associated action. Previously, an unavailable checker could let a call continue through the normal permission flow. The practical change is that a required policy service can refuse to let work proceed when it cannot answer. Release 2.1.295.

Imagine a central service checking whether a tool may export customer records to a destination. A policy refusal should return a successful HTTP response carrying the event's JSON denial. With onFailure: "block", a connection error or broken response can also stop the export at PreToolUse. Without it, the failed check is normally nonblocking.

Checker resultWith an applicable synchronous hook and onFailure: "block"
Executable or script cannot startTreat as a blocking failure.
Command exits other than 0 or 2Treat as failure, even if stdout contains allowing JSON.
Hook times outApply the event's blocking behavior.
HTTP connection fails or status is non-2xxTreat as failure.
Parsed JSON is invalid or fails its schemaTreat as failure. Command plain text alone is not necessarily invalid output.

An empty successful HTTP response is still success. A command that exits successfully without a decision is not automatically a refusal. onFailure handles a broken checker; it cannot repair a checker whose logic incorrectly approves the action. Failure behavior and HTTP responses.

For team enforcement, put policy configuration and checker code under administrator control. allowManagedHooksOnly restricts hook sources while retaining managed hooks, SDK hooks, and hooks from force-enabled plugins. A project file the agent can rewrite is not enough to establish a protected policy. Managed-hook settings.

The operational tradeoff is straightforward: making a central service mandatory makes its availability part of your developers' workflow. Use this for consequential checks such as destination approval or required pre-action recording. Keep optional formatting and convenience alerts out of that dependency.

Where prompt hooks improved

Also on October 8, 2026, 2.1.294 improved how instruction-style prompt hooks on Stop and SubagentStop are judged, reducing premature stopping. It also fixed instruction-style prompt and agent hooks allowing actions they should have refused. Release 2.1.294.

That helps an acceptance check such as “continue until the requested error handling is covered.” It does not turn a language-model verdict into evidence that a test command succeeded. Use the command example for executable checks and prompt hooks for judgments that genuinely require interpretation.

Rollout and debugging

Validate the settings and exercise the scripts locally before depending on them. The documented terminal diagnostics are:

Bash
claude --version
claude doctor

For example, from your project root, this offline smoke check sends a synthetic event to the guard and prints its denial JSON. It does not run the command named inside the payload:

Bash
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf build"}}' | node .claude/hooks/guard.mjs

This checks the script's output. It does not prove Claude Code's matcher or runtime behavior. For the test hook, check a passing suite, a failing suite, and stop_hook_active: true. For the audit hook, inspect the JSONL records and verify that you are comfortable retaining the selected metadata.

Settings edits are normally picked up automatically. The /hooks menu shows configured hooks; restart if a file-watcher miss leaves an edit unloaded. Use claude doctor, not the old page's claude config doctor command. CLI reference and hook troubleshooting.

For runtime diagnosis, --debug-file selects a predictable destination:

Bash
claude --debug-file ./claude-hooks-debug.log

Since October 9, 2026, 2.1.296 records the command, originating plugin when applicable, outcome, and duration for command hooks on tool calls, prompts, SessionStart, and Stop. This makes an unexpectedly slow checker identifiable. Release 2.1.296.

Do not expect timing for every hook: events including Notification, SessionEnd, and PreCompact can have completion entries without duration. Plain --debug writes a log file rather than printing those details to the terminal. Debug-hook reference.

Start with the repository's actual test command, decide which policy failures should stop tool execution, and assign ownership of those checks. For related environment decisions, see Cursor Cloud Agent environments vs Cloudflare Workers. The Cloudflare production-agents article covers the wider system context.

What v2.1.141 changed on May 13, 2026

The May release story remains useful history. 2.1.141, released May 13, 2026, added terminalSequence so Claude Code could emit terminal notifications on a hook's behalf. Earlier, 2.1.139 introduced exec-form args, a PostToolUse continuation option, and CLAUDE_PROJECT_DIR for stdio MCP servers; 2.1.133 exposed the active effort level to hooks. Release 2.1.141 and Anthropic's changelog.

Use the current contract when migrating: args accompanies command; terminalSequence is limited to an interactive on-screen interface and is ignored in non-interactive -p and SDK use; current continueOnBlock guidance applies to prompt-hook decisions by event. A command PostToolUse rejection provides feedback after execution. The old maxAttempts example is not a documented current hook setting. Effort input also depends on the event context and model support, rather than being guaranteed on every hook. Current hook reference.

Reader questions

Can a Stop hook guarantee that tests passed?

No. A Stop hook can run tests and request more work, but interruptions, hook failures, and loop protections limit that behavior. The example intentionally exits early on a continuation. Enforce mandatory pass conditions in your merge or deployment checks.

Should I use a command hook or a prompt hook for tests?

Use a command hook to execute the suite and read its result. A prompt hook is useful for interpreting acceptance criteria, but a favorable model judgment does not establish that tests ran.

Does PostToolUse record failed calls too?

No. Register PostToolUseFailure for failed calls and PreToolUse for attempts. Correlate records by tool-use and session identifiers, and account for calls that never execute and the documented EndConversation exception.

Does args replace command in settings.json?

No. In exec form, command names the executable and args contains its arguments. Omitting args selects shell form.

For more practical setup guides, join the newsletter.

Last Updated
Category
Build
Related Articles
GitHub Copilot Fusion: How It Works and When to Use It

GitHub Copilot Fusion: How It Works and When to Use It

Understand Copilot Fusion, its model orchestration, supported clients and credit usage, plus a practical one-repository trial and reasons to leave it off.Oct 29, 2026Build
Bright Data Review

Bright Data Review

Bright Data reviewed for agents, RAG and price monitoring: product fit, billing, free trials, residential KYC, and when to choose Firecrawl or Browse AI.Oct 28, 2026Build
Dify vs n8n

Dify vs n8n

Dify vs n8n for AI apps, business workflows and self-hosting. Compare features, commercial licence rules and the job each platform fits.Oct 27, 2026Build
Zapier Alternatives in 2026: Make, n8n, Gumloop, Pipedream, Activepieces and Power Automate

Zapier Alternatives in 2026: Make, n8n, Gumloop, Pipedream, Activepieces and Power Automate

Compare Zapier alternatives by task volume, self-hosting, AI agents, code and Microsoft 365. Current billing models and what migration requires.Oct 27, 2026Build
AutoGen vs LangGraph

AutoGen vs LangGraph

AutoGen is archived. Compare its conversation model with LangGraph's stateful workflows, then choose LangGraph or Microsoft Agent Framework for migration.Oct 27, 2026Build
LoRA Fine Tuning: QLoRA, Adapters and Evaluation

LoRA Fine Tuning: QLoRA, Adapters and Evaluation

Learn how LoRA and QLoRA work, when to fine-tune, which hosted and GPU routes train adapters, and how to evaluate and serve them.Oct 26, 2026Build
LangChain Alternatives in 2026: LangGraph, LlamaIndex, Pydantic AI and More (Compared)

LangChain Alternatives in 2026: LangGraph, LlamaIndex, Pydantic AI and More (Compared)

Compare LangGraph, LlamaIndex, Pydantic AI and six more LangChain alternatives by language, models, licence and the problem each solves.Oct 26, 2026Build
LLM vs SLM: Which Model Fits Your Feature?

LLM vs SLM: Which Model Fits Your Feature?

Choose large or small language models by task. Compare current model families, deployment limits and a routing example for production features.Oct 26, 2026Build
Newsletter

One letter, every Sunday.Working systems, not hot takes.

Weekly. No spam. Unsubscribe anytime.