[BUG] Claude Code in Action / Implementing the hook course teach us proccess.exit(2) + console.error

Status Fixed / completed
Reported on v2.1.126
Maintainer reply None cached
Activity 3 comments · opened May 1, 2026 · closed May 1, 2026

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?

Write now I am studying a course Claude Code in Action
Inside that course in section Implementing the hook - we have not actual information.
The course teach us to use
proccess.exit(2) + console.error
if we want to block something inside PretoolUse hook.

But right now we need to use
process.stdout + process.exit(0);

the hooks course teaches process.exit(2) + console.error to block PreToolUse, but this no longer
works in the current version — the correct approach is JSON to stdout with {"continue": false, "stopReason": "..."} +
process.exit(0)

What Should Happen?

Update the information inside the course.

  • you have an antropic certification exame. If inside this exam you use also old information - it is a critical bug

Error Messages/Logs

● Found the issue. The hook at hooks/read_hook.js is correctly detecting .env reads, but it's broken in two ways:

  1. process.exit(2) is the wrong signal. Exit code 2 is only meaningful for asyncRewake hooks. For a synchronous
  PreToolUse hook, it doesn't block the operation — it just indicates an error, and Claude Code falls through and allows
   the read.
  2. console.error writes to stderr, not stdout. Claude Code reads control decisions from stdout. The block signal must
  be JSON on stdout.

  The fix: output {"continue": false, "stopReason": "..."} to stdout, then exit 0:

Steps to Reproduce

Add PreToolHooks

"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "node ./hooks/read_hook.js"
}
]
},

Create a read.hooks.js

async function main() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const toolArgs = JSON.parse(Buffer.concat(chunks).toString());

// readPath is the path to the file that Claude is trying to read
const readPath =
toolArgs.tool_input?.file_path || toolArgs.tool_input?.path || "";

// TODO: ensure Claude isn't trying to read the .env file
if(readPath.includes('.env')){
console.error("You cannot read the .env file")
process.exit(2);
}
}
main();

Then ask claude to read .env file

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Code 2.1.126.

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Windows Terminal

Additional Information

_No response_

View original on GitHub ↗

3 Comments

yurukusa · 5 months ago

The course is correct. exit 2 + stderr is officially supported in the current docs and is not deprecated.

From the official Claude Code Hooks documentation (https://code.claude.com/docs/en/hooks):

Exit 2 means a blocking error. Claude Code ignores stdout and any JSON in it. Instead, stderr text is fed back to Claude as an error message.

And for PreToolUse specifically, exit 2 blocks the tool call.

What is deprecated is the top-level decision / reason JSON fields for PreToolUse. The newer JSON form uses hookSpecificOutput.permissionDecision: "deny" with permissionDecisionReason:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "..."
  }
}

The docs explicitly note: "PreToolUse previously used top-level decision and reason fields, but these are deprecated for this event. Use hookSpecificOutput.permissionDecision and hookSpecificOutput.permissionDecisionReason instead."

So both paths work in the current version:

  • process.exit(2) + console.error(...) — simplest, still officially supported, what the course teaches
  • process.stdout.write(JSON.stringify({hookSpecificOutput: {permissionDecision: "deny", ...}})) + process.exit(0) — more granular control (also lets you "allow" past permission rules, etc.)

If you saw exit 2 actually failing to block on 2.1.126, it would help to share the exact hook script + the matcher in settings.json + the raw stdin payload your hook received — there are separate failure modes (matcher not firing, tool_input.command empty, settings.json key mismatch) that can look like "exit 2 doesn't block" but are upstream issues.

Med1iK · 5 months ago

Agree.
My apologies. It was my mistake.
I forget about
~/.claude/settings.json
and
~/.claude/settings.local.json

github-actions[bot] · 3 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.