[BUG] Claude Code in Action / Implementing the hook course teach us proccess.exit(2) + console.error
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_
3 Comments
The course is correct.
exit 2 + stderris officially supported in the current docs and is not deprecated.From the official Claude Code Hooks documentation (https://code.claude.com/docs/en/hooks):
And for
PreToolUsespecifically, exit 2 blocks the tool call.What is deprecated is the top-level
decision/reasonJSON fields forPreToolUse. The newer JSON form useshookSpecificOutput.permissionDecision: "deny"withpermissionDecisionReason:The docs explicitly note: "PreToolUse previously used top-level
decisionandreasonfields, but these are deprecated for this event. UsehookSpecificOutput.permissionDecisionandhookSpecificOutput.permissionDecisionReasoninstead."So both paths work in the current version:
process.exit(2)+console.error(...)— simplest, still officially supported, what the course teachesprocess.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 2actually failing to block on2.1.126, it would help to share the exact hook script + the matcher insettings.json+ the raw stdin payload your hook received — there are separate failure modes (matcher not firing,tool_input.commandempty, settings.json key mismatch) that can look like "exit 2 doesn't block" but are upstream issues.Agree.
My apologies. It was my mistake.
I forget about
~/.claude/settings.json
and
~/.claude/settings.local.json
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.