claude-code-integration-research.md
Rama master · réplica de lectura del repositorio original.
# Claude Code Integration Research
Research notes from studying jc (a Rust/gpui app that wraps Claude Code) to inform
building similar functionality in iTerm2.
## How jc Communicates with Claude Code
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.
### Sending Input
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.
### Receiving State
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.
## State Tracking
Core state in jc's `SessionState`:
- `busy: bool` -- true while Claude is actively working
- `has_ever_been_busy: bool` -- true once Claude has worked at least once
- `pending_events: HashSet<PendingEvent>` -- tracks permission prompts, API errors, terminal bells
Transitions:
- `PromptSubmit` -> `busy = true`
- `Stop` / `IdlePrompt` / `PermissionPrompt` / `StopFailure` -> `busy = false`
## Diff Discovery
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/index` mtime 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.
## Tricky Edge Cases jc Solved
1. **`/clear` two-event correlation**: `/clear` fires `SessionEnd` then
`SessionStart` as separate hooks. jc stashes the `SessionEnd` with a
10-second expiry and pairs it with the subsequent `SessionStart` by
project path.
2. **UUID bootstrapping**: Fresh `claude` sessions start with `uuid = 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
pending `uuid=None` session in the matching project.
3. **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.
4. **Escape sanitization**: `write_text()` strips `\x1b` from pasted content
to prevent premature termination of bracketed paste mode.
5. **L2 problem suppression**: "Unsent WAIT" notifications are suppressed when
Claude is busy or higher-priority problems exist, to avoid noise.
6. **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.
7. **Multi-instance hook collision**: Hook uninstall removes ALL `/jc-hook/`
URLs, which would break a second instance's hooks.
8. **File watcher self-write suppression**: An `AtomicBool` flag suppresses
filesystem watcher events for 200ms after jc itself writes a file.
9. **`usize::MAX` sentinel**: The refresh channel uses `usize::MAX` as a
"wake up and re-evaluate" signal distinct from project-index-targeted
refreshes.
10. **Clipboard polling for `/copy`**: After sending `/copy` to 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 UI Structure
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 `/copy` replies
- **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.
## Implications for iTerm2
### Hook transport
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.
### What iTerm2 needs from hooks
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 `/clear` for session identity
Other events (PreToolUse, PostToolUse, SubagentStart, etc.) could power
richer UI like tool activity indicators or progress tracking.
### Existing iTerm2 mechanisms that could help
- **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.
### Swift types
`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.