Research notes from studying jc (a Rust/gpui app that wraps Claude Code) to inform building similar functionality in iTerm2.
The communication is asymmetric:
- jc -> Claude Code: PTY stdin (keystrokes and bracketed-paste text)
- Claude Code -> jc: HTTP hooks (Claude Code POSTs JSON to a localhost server)
There is no bidirectional API. jc is essentially a terminal emulator (using alacritty's VTE) with a hook server bolted on.
Claude Code runs as a PTY subprocess via the portable_pty crate. All input
goes through the PTY writer:
- Keystrokes: Converted to terminal escape sequences and written directly.
- Text paste: Wrapped in bracketed paste mode (
\x1b[200~...\x1b[201~), with ESC bytes stripped to prevent escape injection. Enter (\r) is sent after a 200ms delay on a background thread to give Claude Code time to process the paste. - Commands:
/copy, prompt text, etc. are pasted the same way.
jc installs hooks into .claude/settings.local.json pointing at a localhost
HTTP server (using tiny_http, ~190 lines). Claude Code fires POST requests
when its state changes. The hook server parses the JSON body and sends events
through a flume channel to the main app.
Hook event types jc uses:
| Hook | Meaning |
|---|---|
UserPromptSubmit |
User submitted a prompt -- Claude is about to work |
Stop |
Claude finished normally |
StopFailure |
API error during execution |
Notification (idle_prompt) |
Claude finished, waiting for new input |
Notification (permission_prompt) |
Permission dialog shown |
PermissionRequest |
Tool permission dialog appeared |
SessionEnd |
Session ended (used to detect /clear) |
SessionStart |
Session started (paired with SessionEnd for /clear) |
Not all hook types support "type": "http". SessionEnd and SessionStart
only support "type": "command", so jc works around this by using command hooks
that pipe stdin through curl back to its own HTTP server.
Core state in jc's SessionState:
busy: bool-- true while Claude is actively workinghas_ever_been_busy: bool-- true once Claude has worked at least oncepending_events: HashSet<PendingEvent>-- tracks permission prompts, API errors, terminal bells
Transitions:
PromptSubmit->busy = trueStop/IdlePrompt/PermissionPrompt/StopFailure->busy = false
jc does NOT rely on hooks to learn which files changed. It runs
git2::Repository::diff_tree_to_workdir_with_index() to compare HEAD against
the working tree. Diffs are refreshed:
- Every 2 seconds when the window is active (working-tree diffs are always
considered stale since
.git/indexmtime doesn't reflect unstaged edits) - Immediately when a hook event fires (Stop, StopFailure, etc.)
Each file diff gets a checksum. A reviewed hashmap tracks which files the user
has marked as reviewed. If the diff content changes, the reviewed state is
invalidated.
-
/cleartwo-event correlation:/clearfiresSessionEndthenSessionStartas separate hooks. jc stashes theSessionEndwith a 10-second expiry and pairs it with the subsequentSessionStartby project path. -
UUID bootstrapping: Fresh
claudesessions start withuuid = None. The UUID is discovered on the first hook event. jc does a two-pass match: first by UUID across all projects, then falls back to assigning to a pendinguuid=Nonesession in the matching project. -
200ms paste delay: Enter is sent on a background thread after 200ms to let the PTY buffer process pasted content. There's no "paste complete" acknowledgment.
-
Escape sanitization:
write_text()strips\x1bfrom pasted content to prevent premature termination of bracketed paste mode. -
L2 problem suppression: "Unsent WAIT" notifications are suppressed when Claude is busy or higher-priority problems exist, to avoid noise.
-
L0 cross-session "home": Permission prompts can require jumping to a different session. The system saves a "home" session on the first jump and returns after all L0 problems are resolved.
-
Multi-instance hook collision: Hook uninstall removes ALL
/jc-hook/URLs, which would break a second instance's hooks. -
File watcher self-write suppression: An
AtomicBoolflag suppresses filesystem watcher events for 200ms after jc itself writes a file. -
usize::MAXsentinel: The refresh channel usesusize::MAXas a "wake up and re-evaluate" signal distinct from project-index-targeted refreshes. -
Clipboard polling for
/copy: After sending/copyto Claude Code's PTY, jc polls the system clipboard every 200ms for up to 3 seconds waiting for it to change. No API exists to extract Claude's last response directly.
jc is a multi-pane IDE built on gpui (Zed's GPU-accelerated UI framework). Default layout is three resizable side-by-side panes, each of which can be:
- Claude Terminal -- Claude Code's TUI in a full terminal emulator
- General Terminal -- a regular shell
- Code Viewer -- syntax-highlighted editor (tree-sitter) for files and
/copyreplies - Git Diff -- diff view with reviewed/unreviewed tracking
- TODO Editor -- per-project TODO.md with session management
- Global TODO -- cross-project todo list
Claude Code's output is shown directly in the terminal pane. The /copy
feature extracts the last reply into the code viewer as a convenience.
Command hooks work for all event types; HTTP hooks don't (SessionEnd/SessionStart
are command-only). For iTerm2, command hooks are the natural choice since they
can invoke it2 to talk to iTerm2 over its existing API endpoint (raw protobuf).
This avoids running an HTTP server entirely.
The hook command would be something like:
it2 claude-hook --session-id $SESSION_ID
with JSON on stdin, and it2 forwards it to iTerm2 via the API socket.
For parity with jc's functionality:
- UserPromptSubmit: Mark session as busy (spinner, status indicator)
- Stop / Notification(idle_prompt): Mark session as idle
- StopFailure: Show error indicator
- Notification(permission_prompt) / PermissionRequest: Show attention badge
- SessionEnd / SessionStart: Track
/clearfor session identity
Other events (PreToolUse, PostToolUse, SubagentStart, etc.) could power richer UI like tool activity indicators or progress tracking.
- Triggers: Regex on terminal output, useful for detecting patterns Claude Code renders (progress bars, prompts). Complement hooks for things not covered by the hook protocol.
- Badges/marks: Visual indicators for session state.
- Python API /
it2: IPC channel for hook commands to report events.
ClaudeCodeHookEvent.swift was written to it2cli/Sources/it2/ with Codable
structs covering all 25 hook event types from the official spec at
https://code.claude.com/docs/en/hooks.