[BUG] PostToolUse hook output visibility depends on exit code and stream

Resolved 💬 5 comments Opened Nov 7, 2025 by pro-vi Closed Jan 18, 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?

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

  1. Document this behavior in Claude Code docs (hooks section)
  2. 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"
  1. 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)

  1. Create ~/.claude/hooks/test-exit-0.ts:

``ts
#!/usr/bin/env bun
console.log("Test message - exit 0");
process.exit(0);
``

  1. Make executable: chmod +x ~/.claude/hooks/test-exit-0.ts
  2. Add to settings.json:

``json
"PostToolUse": [{
"hooks": [{
"type": "command",
"command": "bun ~/.claude/hooks/test-exit-0.ts"
}]
}]
``

  1. 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)

  1. Modify ~/.claude/hooks/test-exit-0.ts:

``ts
#!/usr/bin/env bun
console.log("Test message - exit 1");
process.exit(1);
``

  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)

  1. Modify ~/.claude/hooks/test-exit-0.ts:

``ts
#!/usr/bin/env bun
console.error("Test message - exit 2");
process.exit(2);
``

  1. 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)

  1. Modify ~/.claude/hooks/test-exit-0.ts:

``ts
#!/usr/bin/env bun
console.log("Test message - exit 2 stdout");
process.exit(2);
``

  1. 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_

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗