[BUG] PostToolUse hook output visibility depends on exit code and stream
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?
Summary
PostToolUse hooks have three different visibility modes controlled by exit codes and output streams
(stdout vs stderr), but this behavior is undocumented. This makes it difficult to create hooks with
appropriate visibility for different use cases.
## Problem
When implementing a PostToolUse hook, output visibility varies unexpectedly:
- Some output appears only to the user
- Some output appears to both user and Claude
- The labeling ("success (0)", "error (1)", "blocking error (2)") doesn't clearly indicate visibility
- No documentation exists explaining this behavior
This leads to confusion when trying to create:
- Monitoring hooks (user should see output, Claude shouldn't be interrupted)
- Action hooks (both user and Claude need to see output)
- Silent hooks (neither should see output under normal conditions)
## Discovered Behavior
Through empirical testing, PostToolUse hooks have three visibility modes:
### Mode 1: User-only visibility (non-intrusive monitoring)
``ts``
console.log(message); // or process.stdout.write()
process.exit(0);
Result:
- ✅ User sees:
⎿ PostToolUse:ToolName hook succeeded: message - ❌ Claude doesn't see it (not in context)
- Use case: Confirmation, monitoring, non-critical logging
### Mode 2: User-only visibility (with error label)
``ts``
// Any output method
process.exit(1); // or exit(3+)
Result:
- ✅ User sees:
⎿ PostToolUse:ToolName hook error: Failed with non-blocking status code: message - ❌ Claude doesn't see it
- Use case: Non-critical warnings visible to user
### Mode 3: Both user and Claude see it
``ts``
console.error(message); // or process.stderr.write()
process.exit(2);
Result:
- ✅ User sees:
⎿ PostToolUse:ToolName hook blocking error: [hook-cmd path/to/script]: message - ✅ Claude sees: <system-reminder>PostToolUse:ToolName hook blocking error:
[hook-cmd path/to/script]: message</system-reminder>
- Use case: Critical events requiring Claude's attention/action
## Environment
- Claude Code version: 2.0.35
- Hook type: PostToolUse
- Tested: exit codes 0, 1, 2, 3
- Tested: stdout, stderr, console.log, console.error
## Related Hooks
Note: UserPromptSubmit hooks appear to always be visible to Claude (as <system-reminders>) regardless
of exit code. PostToolUse hooks are unique in having selective visibility.
What Should Happen?
Suggested Improvements
- Document this behavior in Claude Code docs (hooks section)
- Clearer labeling - Current labels are misleading:
- "succeeded" implies exit 0, but doesn't indicate visibility
- "blocking error" sounds bad but is the only way to reach Claude
- Consider: "user-visible", "claude-visible", or "internal"
- Explicit visibility control - Consider adding a visibility field to hook output:
```json
{
"summary": "message",
"visibility": "user-only" | "claude-only" | "both" | "none"
}
When `none`, omit `:` in message `PostToolUse:Edit hook succeeded:`
4. Hook testing guide - Potentially provide /debug-hooks showing what Claude sees vs user sees
### Error Messages/Logs
```shell
// claude don't see the message
⎿ PostToolUse:ToolName hook succeeded: message
// claude don't see the message
⎿ PostToolUse:ToolName hook error: Failed with non-blocking status code: message
// claude sees the message
⎿ PostToolUse:ToolName hook blocking error: [hook-cmd path/to/script]: message
Steps to Reproduce
## Steps to Reproduce
### Test 1: Exit 0 with stdout (user-only visibility)
- Create
~/.claude/hooks/test-exit-0.ts:
``ts``
#!/usr/bin/env bun
console.log("Test message - exit 0");
process.exit(0);
- Make executable:
chmod +x ~/.claude/hooks/test-exit-0.ts - Add to settings.json:
``json``
"PostToolUse": [{
"hooks": [{
"type": "command",
"command": "bun ~/.claude/hooks/test-exit-0.ts"
}]
}]
- Use any tool in Claude Code (e.g., Read a file)
Expected: Output visible to both user and ClaudeActual:
- User sees:
⎿ PostToolUse:Read hook succeeded: Test message - exit 0 - Claude does NOT see it in context (you may ask Claude about it)
---
### Test 2: Exit 1 with stdout (user-only visibility with error label)
- Modify ~/.claude/hooks/test-exit-0.ts:
``ts``
#!/usr/bin/env bun
console.log("Test message - exit 1");
process.exit(1);
- Use any tool in Claude Code
Expected: Output visible to both user and ClaudeActual:
- User sees: `⎿ PostToolUse:Read hook error: Failed with non-blocking status code: Test message -
exit 1`
- Claude does NOT see it in context
---
### Test 3: Exit 2 with stderr (visible to both)
- Modify ~/.claude/hooks/test-exit-0.ts:
``ts``
#!/usr/bin/env bun
console.error("Test message - exit 2");
process.exit(2);
- Use any tool in Claude Code
Expected: Output visible to both user and ClaudeActual:
- User sees:
⎿ PostToolUse:Read hook blocking error: Test message - exit 2 - Claude DOES see it: <system-reminder>PostToolUse:Read hook blocking error: Test message - exit
2</system-reminder>
---
### Test 4: Exit 2 with stdout (NOT visible)
- Modify ~/.claude/hooks/test-exit-0.ts:
``ts``
#!/usr/bin/env bun
console.log("Test message - exit 2 stdout");
process.exit(2);
- Use any tool in Claude Code
Expected: Output visible based on exit codeActual:
- User sees:
⎿ PostToolUse:Read hook blocking error: (empty) - Claude sees: <system-reminder>PostToolUse:Read hook blocking error: </system-reminder> (empty)
Conclusion: Exit code 2 requires stderr, stdout is ignored
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.0.35 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
_No response_
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗