# jcode hook events What jcode actually emits, and why Orca's status mapping is shaped the way it is. Everything below was captured from jcode **v0.87.1 (944f747e9)** by pointing every `[hooks]` entry in `config.toml` at a script that appends `$JCODE_HOOK_PAYLOAD` to a log, then running real turns. Re-capture before changing the mapping; do not edit it from memory. ## The six events jcode's `[hooks]` table (`crates/jcode-base/src/hooks.rs`) has six lifecycle points. Five are **observers** — detached, fire-and-forget, they can never slow the agent. One, `pre_tool`, is a **gate**: jcode spawns it, writes the tool input to its stdin, and waits for it to exit before the tool runs. | Event | When | Orca state | Notable payload fields | | --------------- | -------------------------------------------- | ---------- | --------------------------------------------------------- | | `session_start` | TUI open, attach, or `--resume` | none | `source` = `create`/`attach`/`resume`, `model` | | `turn_start` | prompt submitted, before the model generates | `working` | `source`, `model` | | `pre_tool` | before each tool call (gate) | `working` | `tool_name`, `tool_input` (argument JSON as a string) | | `post_tool` | after each tool call | `working` | `tool_name`, `status`, `duration_ms`, `output_bytes`/`error` | | `turn_end` | turn finished | `done` | `status`, `duration_ms`, `model`, `last_assistant_text`, `error` | | `session_end` | session closed | `done` | `source` = `close` | `session_start` is identity-only. jcode fires it on an idle TUI open, so mapping it to `working` would spin before the user has typed anything (same reason Devin does not map its `SessionStart`). ## Captured payloads A `jcode run` turn that read one file and wrote another: ```json {"cwd":"/private/tmp/jcode-work","event":"session_start","model":"claude-haiku-4-5","session_id":"session_pawprint_1790149117838_071c2a106812396c","source":"create"} {"cwd":"/private/tmp/jcode-work","event":"pre_tool","session_id":"session_pawprint_…","tool_input":"{\"file_path\":\"sample.txt\",\"intent\":\"Read sample.txt to get its contents\"}","tool_name":"read"} {"cwd":"/private/tmp/jcode-work","duration_ms":"0","event":"post_tool","output_bytes":"12","session_id":"session_pawprint_…","status":"ok","tool_name":"read"} {"cwd":"/private/tmp/jcode-work","event":"pre_tool","session_id":"session_pawprint_…","tool_input":"{\"content\":\"HELLO\",\"file_path\":\"out.txt\",\"intent\":\"Write uppercased contents of sample.txt to out.txt\"}","tool_name":"write"} {"cwd":"/private/tmp/jcode-work","duration_ms":"9","event":"post_tool","output_bytes":"137","session_id":"session_pawprint_…","status":"ok","tool_name":"write"} ``` A TUI turn that failed upstream (note `turn_start`, which the `run` path does not emit): ```json {"cwd":"/private/tmp/jcode-work","event":"session_start","model":"claude-opus-5","session_id":"session_snail_…","source":"create"} {"cwd":"/private/tmp/jcode-work","event":"turn_start","model":"claude-opus-5","session_id":"session_snail_…","source":"chat"} {"cwd":"/private/tmp/jcode-work","duration_ms":"6868","error":"Anthropic API error (503 Service Unavailable): …","event":"turn_end","model":"claude-opus-5","session_id":"session_snail_…","status":"error"} ``` Three consequences the mapping depends on: - **`turn_start` only fires on the streaming turn path** (TUI, desktop, swarm workers, headless sessions), not `jcode run`. It is what fills the otherwise blank window between a submitted prompt and the first tool call. - **Only `pre_tool` carries `tool_input`.** `post_tool` reports the name and the outcome, so the tool preview has to be held from the matching `pre_tool`. - **Every jcode tool schema has an `intent` string** the model fills in. It is the preview fallback when no tool-specific key (`file_path`, `command`, …) matches. ## Why Orca subscribes to the gate `pre_tool` is the only event that can report a tool *while it runs*. Without it a three-minute `bash` shows no tool at all until it finishes. Two rules keep the gate from ever costing the agent anything: 1. **The POST is detached.** jcode calls `child.wait_with_output()`, which waits for the process *and* reads its stderr to EOF — a backgrounded child that inherited stderr would hold the gate open for as long as it ran. The managed script runs the POST as `orca_post_jcode_event >/dev/null 2>&1 &`, so the inherited pipes are closed and the script exits immediately. 2. **stdin is drained first.** jcode `write_all`s the full tool input to the hook's stdin. A tool input larger than the pipe buffer (a big `write`) would block that write until the gate timed out if nobody read it, so the script drains stdin before any exit path. Orca never blocks a jcode tool call: the script always exits 0. ## Questions and permissions jcode has **no interactive per-tool approval prompt**. Its safety model (`crates/jcode-app-core/src/tool/bash_destructive_gate.rs`) either denies a command outright or asks the model to justify it — both inside the tool, with no human in the loop. There is therefore no hook, and no terminal-title state, for "jcode is waiting on you" during ordinary tool use. The one tool a *human* answers is ambient mode's `request_permission` (`crates/jcode-app-core/src/tool/ambient.rs`), resolved out of band with `jcode permissions`. Orca maps a `pre_tool` for it to `waiting` and publishes the tool input as the question card. `post_tool` for the same tool is *not* mapped — by then the human has already answered. Matching is by exact tool name. jcode's live tool set is `agentgrep, apply_patch, bash, batch, bg, browser, compile_remote, conversation_search, edit, gmail, integration_tools, ls, macos_computer_use, maintainer_feedback, mcp, memory, multiedit, open, panel, patch, read, schedule, session_search, side_panel, skill_manage, swarm, todo, webfetch, websearch, write` plus the ambient tools; a substring rule over that set would be matching on coincidence. ## Terminal titles jcode paints OSC 0 titles roughly once a second. Captured sequence from one TUI session: ``` jcode → 🐍 jcode Snake → 🐍 jcode/creek Snake → 🌐 jcode Snake · work ~0s → … → 🌐 jcode Snake · last ~6s ``` The format is ` jcode [ · +N -M][ · work|last ~]` (`crates/jcode-tui/src/tui/app/terminal_title.rs`). Orca uses it for tab-bar identity only — status comes from hooks, never from a parsed title. Note there is no "needs input" title state; that is the same gap as above, not an omission in the parser. ## Per-pane daemons jcode runs one server/client daemon per runtime dir, and lifecycle hooks fire *inside the daemon*. Every TUI client connects the daemon the first pane started, so without isolation a second jcode pane's events carry the first pane's `ORCA_PANE_KEY` and its status lands on the wrong tab. jcode does forward a client's terminal identity to hooks (`CLIENT_TERMINAL_ENV_VARS` in `crates/jcode-terminal-launch/src/lib.rs`), but that allowlist covers tmux/zellij/herdr and the terminal emulators — not `ORCA_PANE_KEY`. Until it does, Orca stamps a per-pane `JCODE_RUNTIME_DIR` so each pane gets its own daemon, socket, and lock. The value is a 16-hex hash of the pane key because the socket path is capped at `SUN_LEN` (104 bytes) and a full pane key never fits. ## Windows hook launcher Use Jcode **v0.89.0 or newer** on Windows. Earlier observer hooks launch with `DETACHED_PROCESS`, leaving their children without a console to inherit. A console program such as the managed hook's `curl.exe` can then open a Windows Terminal tab on every event. Jcode's launcher fix uses `CREATE_NO_WINDOW` for observer hooks and the `pre_tool` gate, keeping their descendants invisible. Changing the managed script alone cannot repair an older Jcode launcher. The managed Windows hook redirects its payload file into curl directly, avoiding the extra shells that a `type ... | curl` pipeline starts. Existing managed scripts are refreshed on Orca startup without changing the user's hook configuration. Report: https://github.com/stablyai/orca/pull/22539#issuecomment-5809618574 Launcher fix: https://github.com/1jehuang/jcode/pull/1490 ## Config shape `[hooks]` values accept a string or an array of strings (`HookCommands` in `crates/jcode-config-types/src/lib.rs`), and jcode re-reads the config on reload, so hooks can be added without restarting. jcode parses a hook command line shell-style but **executes it directly, not through a shell** — the managed value must be the script path, never an `if [ -f … ]` wrapper. That shell-style parse is `parse_hook_command` (`crates/jcode-terminal-launch/src/lib.rs`), and it is why Orca stores the path **shell-quoted**. The tokenizer splits on unquoted whitespace and consumes every unquoted backslash as an escape, so a bare Windows path reaches `exec` as `C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fires at all; a POSIX home with a space splits into two arguments. Single quotes pass a path through verbatim — backslashes are literal inside them — so Orca single-quotes by default and falls back to double quotes (escaping `\` and `"`) only for a path that itself contains a single quote. The value is then TOML-quoted on the way into the file, so neither the raw path nor the shell-quoted string appears alone.