Skip to content

fix: TTY detection for hook subprocesses#19

Open
wasikarn wants to merge 1 commit intowarpdotdev:mainfrom
wasikarn:fix/tty-detection-proven
Open

fix: TTY detection for hook subprocesses#19
wasikarn wants to merge 1 commit intowarpdotdev:mainfrom
wasikarn:fix/tty-detection-proven

Conversation

@wasikarn
Copy link
Copy Markdown

@wasikarn wasikarn commented Apr 15, 2026

Problem

Hook subprocesses spawned by Claude Code lack a controlling terminal, causing OSC 777 notifications to fail silently.

Root Cause

When Claude Code spawns hook scripts (e.g., on Stop events), the subprocess does not have a controlling terminal. Writing to /dev/tty fails with "Device not configured", preventing Warp notifications from being sent.

Evidence:

# Original approach
$ printf '\033]777;notify;Test;Body\007' > /dev/tty
bash: /dev/tty: Device not configured

Impact

  • Warp notifications fail intermittently or not at all
  • Users don't receive "Task completed" notifications when Claude finishes work
  • Breaks the core value proposition of the Warp + Claude Code integration

Solution

Walk the parent process chain to find the actual TTY device that Claude Code is running on.

Changes

  1. TTY Detection: Traverse PPID chain until valid TTY is found
  2. Termination Check: Stop at PID 0/1 to prevent infinite loops
  3. Robust Trimming: Use [[:space:]] to handle spaces and tabs
  4. Safe Fallback: Skip notification if TTY not found (don't write to broken /dev/tty)

Key Implementation Details

# Read TTY and PPID in one ps call
read -r tty_val ppid_val < <(ps -o tty=,ppid= -p  2>/dev/null)

# Trim all whitespace
tty_val=

# Skip if not found
if [ -n  ] && [ -w  ]; then
    printf '...' > 
fi

Testing

Verified Working

  • Warp Version: v0.2026.04.08.08.36.stable_05
  • OS: macOS (also tested on Linux via CI)
  • All Tests Pass: 43 passed, 0 failed

Test Coverage

  • TTY detection for current shell
  • Parent chain walking (PPID → TTY)
  • Invalid PID handling
  • Platform-agnostic assertions (works with /dev/ttysXXX on macOS and /dev/pts/N on Linux)

Manual Verification

# Before fix: fails silently
# After fix: Warp notification appears in notification center

Checklist

  • Fix tested locally with Warp stable
  • All existing tests pass (43/43)
  • New tests added for TTY detection logic
  • Tests match production code exactly
  • Platform-agnostic (macOS/Linux)
  • Backward compatible (graceful degradation)
  • Code follows bash best practices
  • No breaking changes

Platform Notes

Unix-like systems (macOS, Linux): ✅ Fully supported
Windows: Not supported by this fix (requires different TTY detection logic)


cc: @harryalbert (previous contributor)

@wasikarn wasikarn force-pushed the fix/tty-detection-proven branch 4 times, most recently from bb7de02 to 2b585f2 Compare April 15, 2026 04:52
Hook subprocesses spawned by Claude Code lack a controlling terminal,
making /dev/tty unavailable. This caused OSC 777 notifications to fail
silently in some contexts.

Changes:
- Walk the parent process chain to find the actual TTY device
- Add termination check for PID 0/1 to prevent infinite loops
- Use [[:space:]] for robust whitespace trimming (spaces + tabs)
- Skip notification if TTY not found (don't fall back to broken /dev/tty)
- Add tests matching production code exactly
- Make tests platform-agnostic (works on macOS /dev/ttysXXX and Linux /dev/pts/N)

Tested: Confirmed working with Warp v0.2026.04.08.08.36.stable_05
Note: This fix targets Unix-like systems (macOS, Linux). Windows support
would require different TTY detection logic.
@wasikarn wasikarn force-pushed the fix/tty-detection-proven branch from 2b585f2 to 36e8254 Compare April 15, 2026 04:54
@wasikarn
Copy link
Copy Markdown
Author

Hi @harryalbert and team,

This PR is ready for review. It fixes the TTY detection issue where hook subprocesses fail to send OSC 777 notifications because /dev/tty is unavailable.

Key changes:

  • Walks parent process chain to find actual TTY device
  • Adds PID 0/1 termination check
  • Uses [[:space:]] for robust whitespace handling
  • Skip notification if TTY not found (no broken fallback)
  • Includes tests that match production code

All 43 tests pass. Tested on Warp v0.2026.04.08.08.36.stable_05.

Please review when you have time. Thanks!

yigitkonur added a commit to yigitkonur/claude-code-warp that referenced this pull request Apr 21, 2026
Covers every hook in the Claude Code lifecycle so the Warp sidebar
reflects real agent state instead of getting stuck on In progress /
Blocked.

State coverage
- PostToolUse now fires for every tool (matcher removed). A `.blocked`
  marker dropped by PermissionRequest gates emission so we don't flood
  Warp with tool_complete on every Read/Glob/Grep — auto-approved tools
  skip emission entirely. Fixes stuck-Blocked without re-introducing the
  200+ GB leak reported in warpdotdev#22.
- New PreToolUse hook emits tool_start for live per-tool sidebar signal.
- SubagentStart/Stop, PermissionDenied, PostToolUseFailure, PreCompact,
  PostCompact, CwdChanged, SessionEnd get dedicated scripts with a
  v3-only event gate (WARP_CLI_AGENT_V3_EVENTS=1). When unset, they emit
  v2-compatible fallbacks so the sidebar state still moves on stable
  Warp builds.

Fresh-tab parity with Gemini/Droid
- on-session-start.sh for source=startup emits nothing. Warp's
  process-detection registers the sidebar row without a state pill,
  matching Gemini CLI and Factory/Droid (neither registers a Warp hook).
  First OSC event fires on UserPromptSubmit. plugin_version moved onto
  prompt_submit so Warp's outdated-plugin banner still has a signal.
- For resume/clear/compact, session_start still emits with enrichment
  since those genuinely change state.

Payload hygiene
- build-payload.sh strips empty-string --arg values so Warp doesn't see
  model:"" / permission_mode:"" as "still initializing".
- Early-exit on empty session_id avoids stray emissions to the wrong row.
- utf8_truncate helper — codepoint-aware, preserves multi-byte integrity.

Reliability
- PPID-walk TTY detection in warp-notify.sh (ports warpdotdev#19). Hook
  subprocesses often lack /dev/tty; walk the parent chain to find one.
- hooks.json quotes ${CLAUDE_PLUGIN_ROOT} (ports warpdotdev#26) — Windows paths
  with spaces no longer break every hook.
- async:true removed from Stop/StopFailure/SubagentStop. Claude Code was
  killing async hooks before they finished emitting in -p mode, causing
  tool_complete / stop emissions to silently drop.
- -p headless mode: on-session-{start,end}.sh skip emission with no tty.

Configurability
- CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 opts out of prompt-as-title
  (ports warpdotdev#24 — users driving tab titles via kitty/tmux/shell hooks).
- WARP_PLUGIN_DISABLE_PROJECT=1 drops the project envelope field
  (ports warpdotdev#23).

Debugging
- New scripts/warp-log.sh logs every hook input and every OSC emit to
  \${TMPDIR:-/tmp}/warp-claude-\${SESSION_ID}.log with sub-second
  timestamps. /tmp/warp-claude-latest.log is a symlink for tail -f.
  WARP_KEEP_LOGS=1 preserves logs past SessionEnd.

Prompt lifecycle
- session_title on prompt_submit labels the sidebar row with the first
  prompt text instead of the generic "Claude Code".
- duration_ms on stop, computed from a t0 timestamp stashed on
  prompt_submit.
yigitkonur added a commit to yigitkonur/claude-code-warp that referenced this pull request Apr 21, 2026
Covers:
  - What the v3 fork changes relative to upstream v2 (table of fixes
    with links to resolved upstream issues warpdotdev#19/warpdotdev#22/warpdotdev#23/warpdotdev#24/warpdotdev#26)
  - Install / uninstall / rollback commands
  - Env-var configuration: WARP_CLI_AGENT_V3_EVENTS, WARP_KEEP_LOGS,
    CLAUDE_CODE_DISABLE_TERMINAL_TITLE, WARP_PLUGIN_DISABLE_PROJECT
  - R5 fallback mapping (v3-only events → v2-compatible shapes)
  - Debugging workflow via `tail -f /tmp/warp-claude-latest.log` with
    a sample event trace from a real session
  - Running the test suite
yigitkonur added a commit to yigitkonur/claude-code-warp that referenced this pull request Apr 21, 2026
3.0.0 → 3.0.5 captures the expanded sidebar state coverage, fresh-tab
zero-emission (Droid/Gemini parity), PostToolUse memory-leak gate, and
five upstream-issue backports: warpdotdev#19 (TTY PPID walk), warpdotdev#22 (leak
mitigation via .blocked marker), warpdotdev#23 (WARP_PLUGIN_DISABLE_PROJECT),
warpdotdev#24 (CLAUDE_CODE_DISABLE_TERMINAL_TITLE), warpdotdev#26 (quote CLAUDE_PLUGIN_ROOT).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant