# jackin' Capsule: In-Container Control Plane (https://jackin.tailrocks.com/reference/capsule/)



The **jackin' Capsule** is the in-container runtime control plane for every Capsule-managed role container. Its executable is `jackin-capsule`, installed at `/jackin/runtime/jackin-capsule` in the derived image and run as PID 1 when the container starts.

The Capsule is not the Docker container, the role image, or an agent runtime. It is the process inside the container that owns PTYs, supervises child processes, renders the in-container multiplexer, exposes the Unix socket API, and performs deterministic runtime setup before each agent starts. The `jackin` host CLI uses that process as the boundary between host orchestration and in-container session state.

## The problem it solves [#the-problem-it-solves]

When jackin' used tmux as the session layer, three structural limitations became apparent:

**No structured session inventory.** Querying which agents were running required `docker exec <container> tmux list-sessions` from the host — a subprocess round-trip that returns a string jackin' then had to parse. There was no typed interface; every session management operation went through shell command construction and string output parsing.

**No agent-state awareness.** tmux knows about terminal sessions and windows. It has no concept of whether the process inside a pane is working, waiting for input, or finished. jackin' could not ask "is this agent blocked?" without reading raw PTY output from outside the container.

**No event stream.** The host could not subscribe to "a session ended" without polling. The only reliable signal was the container stopping, which was too coarse — a container could be running with zero active sessions.

These three gaps are why observability, attention prompts, and future desktop integration were hard to build on top of tmux. Every feature that needed to know what agents were doing required another `docker exec tmux ...` round-trip with output parsing.

## Why build it in-house [#why-build-it-in-house]

The closest public reference is [Herdr](https://github.com/ogulcancelik/herdr) — a Rust multiplexer purpose-built for AI coding agents with four-state status tracking, a Unix socket API, and workspace-level roll-ups. Herdr cannot be embedded in jackin': it is AGPL-3.0, which conflicts with the Apache-2.0 license used by jackin'.

Beyond the license, Herdr wraps **bare host processes**. jackin' wraps **Docker containers**. When Herdr sees `docker attach` rather than the agent process, its PTY heuristics degrade — it is watching the wrong process. `jackin-capsule` runs *inside* the container and reads the agent's PTY output directly, which is exactly where the heuristics need to run.

Building it in-house gives jackin' a control plane that is:

* **Typed and structured.** The socket has a one-shot JSON control channel for inventory queries and a binary attach channel for interactive sessions. The host asks for a session list and gets a typed response, not a string to parse.
* **Agent-aware.** The multiplexer owns every PTY session from spawn to exit. It sees the output directly and can infer state (working / blocked / done / idle) without guessing.
* **Socket-backed.** The daemon is the source of truth for session status and snapshots. The host can query typed state through the control channel today, and the same endpoint is the planned source for host-daemon and desktop event streams.
* **Dependency-free in the image.** tmux is no longer installed in the construct image. The derived image only needs `jackin-capsule` at `/jackin/runtime/jackin-capsule`.

## Process model [#process-model]

`jackin-capsule` operates in three modes:

**Daemon mode (PID 1).** The binary is the container's init process. It:

* Reaps zombie children (mandatory for PID 1 — processes re-parented to PID 1 when their parent exits must be waited on or they accumulate as zombies in the process table)
* Spawns PTY sessions for each agent (one session per pane leaf in the layout tree)
* Renders the status bar and compositor output
* Listens on `/jackin/run/jackin.sock` for client connections and host CLI queries

The host launcher passes the initial agent slug as the container command
argument after the image name, for example `docker run ... jk_the-architect
codex`. The daemon uses that argv value only to choose the first tab.
`JACKIN_AGENT` is set later on each agent PTY before
`/jackin/runtime/entrypoint.sh` runs, so the container's ambient environment
does not imply that every pane is the same agent.

**Attach client mode (PID ≠ 1).** When the operator runs `docker exec -it <container> jackin-capsule`, the binary connects to the daemon socket, bridges the operator's terminal to the active session, and forwards attach frames between the terminal and daemon.

**Runtime setup subcommand (PID ≠ 1).** `/jackin/runtime/entrypoint.sh` calls `jackin-capsule runtime-setup` before each agent exec. The subcommand performs idempotent container setup (git identity, GitHub HTTPS rewrites, and the `gh` credential helper), installs the shared git trailer hook on demand, and refreshes per-agent home/auth handoff state. The hook at `/jackin/state/git-hooks/prepare-commit-msg` is a symlink back to `/jackin/runtime/jackin-capsule`, so Git invokes Rust code directly for `Co-authored-by` and `Signed-off-by` normalization instead of a shell script. The container init marker lives under `/jackin/state`, so global git/GitHub setup runs once per running container; per-session auth handoff still refreshes before every agent launch.

Those modes deliberately keep ownership narrow:

| Owner                           | Lives where                                 | Owns                                                                              | Does not own                                  |
| ------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------- |
| `jackin` host CLI               | Host process                                | Docker lifecycle, image build, socket mount path, final cleanup, operator prompts | Agent PTYs, pane layout, terminal rendering   |
| `jackin-capsule` daemon         | PID 1 inside role container                 | PTYs, tabs/panes, socket listener, render loop, session inventory, child reaping  | Host config, host git state, host credentials |
| `jackin-capsule` attach client  | Short-lived `docker exec` process           | Host terminal raw mode, attach protocol bridge                                    | Session state after detach                    |
| `/jackin/runtime/entrypoint.sh` | Agent child process path                    | Runtime dispatch to Claude/Codex/Amp/Kimi/OpenCode                                | Socket protocol, PTY lifecycle                |
| `jackin-capsule runtime-setup`  | Called by entrypoint before each agent exec | Idempotent in-container setup and auth/home handoff                               | Host-side mutations                           |

The important invariant is that the host starts and stops containers, but the Capsule owns what happens inside a running container after PID 1 starts. Contributors should avoid adding host-side code that scrapes terminal output or shells into the container to infer session state; the socket is the supported boundary.

### Unix Socket and Wire Protocol [#unix-socket-and-wire-protocol]

The daemon listens at `/jackin/run/jackin.sock` inside the container. jackin' bind-mounts a host directory for the instance to `/jackin/run`; the socket file appears inside that directory after the daemon starts:

```
docker run ... -v ~/.jackin/sockets/<container-name>:/jackin/run ...
```

That makes the host-side path `~/.jackin/sockets/<container-name>/jackin.sock`. One socket directory exists per running instance. The socket is a Unix domain socket, not TCP. Its parent directory is locked to `0700` and the socket file is locked to `0600`; the attach channel has no separate authentication layer beyond filesystem ownership, so widening those permissions would allow an in-container peer to inject input into the focused PTY.

The same bind-mounted `/jackin/run` directory also carries `agent.toml`. The host writes `~/.jackin/sockets/<container-name>/agent.toml` before `docker run`; Capsule reads it as `/jackin/run/agent.toml` during startup. This keeps jackin-owned in-container runtime state under the single `/jackin/` root.

Two protocols share the same socket. The first byte sent by a client selects which one the daemon dispatches:

* **Control channel** — first byte `0x00`, interpreted as the first byte of a four-byte big-endian length prefix. The rest of the frame is a UTF-8 JSON request. The daemon reads one request, writes one length-prefixed JSON response, then closes the connection. This is used for one-shot status and snapshot queries.
* **Attach channel** — any non-zero first byte, interpreted as an attach-frame tag. Every frame is `[tag: u8][payload_len: u32 big-endian][payload bytes]`. Interactive terminal bytes stay raw on this hot path; they are not JSON-wrapped or base64-encoded.

Both channels cap a single payload at 4 MiB. The control channel rejects malformed JSON and unknown structural shapes. Unknown tagged message variants decode as `Unknown` so a newer peer can fail fast instead of hanging on a read. The attach channel rejects unknown tags, truncated payloads, and oversized lengths.

The authoritative control-channel wire types live in <RepoFile path="crates/jackin-protocol/src/control.rs">crates/jackin-protocol/src/control.rs</RepoFile> so the host CLI and the in-container binary share the same serde schema without making `jackin` depend on `jackin-capsule`'s PTY and terminal stack. The attach-channel frame codec lives in <RepoFile path="crates/jackin-capsule/src/protocol/attach.rs">crates/jackin-capsule/src/protocol/attach.rs</RepoFile> because it is only used by interactive Capsule clients.

Current control messages:

| Request               | Response                                          | Used by                                                               |
| --------------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
| `{"type":"status"}`   | `{"type":"session_list","sessions":[...]}`        | `jackin-capsule status`, host session inventory, `hardline --inspect` |
| `{"type":"snapshot"}` | `{"type":"snapshot","tabs":[...],"active_tab":N}` | `jackin console` live preview and pane picker                         |

`SessionInfo` carries `id`, `label`, optional `agent`, public `state`, and `active`. `TabSnapshot` carries render-order tabs, each tab's focused pane id, and one `PaneSnapshot` per pane leaf. `PaneSnapshot` carries `session_id`, label, optional agent slug, and state. The public states are `working`, `blocked`, `done`, and `idle`; today they are derived inside `crates/jackin-capsule/src/session.rs`, with the fuller state authority tracked in [Agent runtime status authority](/reference/roadmap/agent-runtime-status/).

Current attach frames:

| Message                | Direction       | Purpose                                                                                                                                                                                |
| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Hello`                | client → daemon | Announce terminal size; optionally request a new shell or agent session; forwards per-session env overrides from `docker exec`; optionally asks focus to move to a specific session id |
| `Input`                | client → daemon | Forward raw keystrokes / paste / mouse bytes                                                                                                                                           |
| `Resize`               | client → daemon | Terminal window resized                                                                                                                                                                |
| `Command`              | client → daemon | Reserved for future structured client commands                                                                                                                                         |
| `Detach`               | client → daemon | Request client detach                                                                                                                                                                  |
| `FocusIn` / `FocusOut` | client → daemon | Outer terminal focus changed                                                                                                                                                           |
| `Welcome`              | daemon → client | Handshake response                                                                                                                                                                     |
| `Output`               | daemon → client | Terminal bytes to render                                                                                                                                                               |
| `SessionList`          | daemon → client | Typed session inventory                                                                                                                                                                |
| `Shutdown`             | daemon → client | All sessions exited; client should exit; failed final sessions include a short reason payload                                                                                          |

The interactive attach path is long-lived. The short-lived `docker exec` client connects, sends `Hello`, then forwards operator terminal input as `Input` frames and terminal resizes as `Resize` frames. The daemon replies with `Welcome`, then streams rendered terminal output as `Output` frames. When the operator detaches, the client sends `Detach` and exits; the daemon keeps every PTY running. When the last live session ends, the daemon sends `Shutdown`, drains the final frame, exits as PID 1, and Docker reports the role container stopped. Clean exits use an empty `Shutdown` payload. Failed final sessions send a short reason payload so the attach client prints the error and exits non-zero instead of returning a silent success.

The attach protocol is intentionally not a general RPC surface. It is optimized for terminal traffic and has three constraints:

1. **Raw terminal bytes stay raw.** Keystrokes, paste payloads, mouse sequences, ANSI output, and OSC passthrough use `Input` / `Output` frames without JSON, escaping, or base64.
2. **Structured control stays out of the hot path.** Inventory and snapshots use the one-shot control channel. Future high-level actions should prefer the control channel unless they are part of an active terminal attach.
3. **The daemon is authoritative.** A client can request a new shell or agent in `Hello`, but it cannot directly mutate the session table. The daemon validates the request, spawns the PTY, and then reports the resulting state.

### How State Moves Across the Boundary [#how-state-moves-across-the-boundary]

The host and Capsule intentionally exchange only a small set of facts:

* **Launch metadata** is written by the host into `/jackin/run/agent.toml` before `docker run` starts the role container. The file contains the role key, workspace workdir, ordered supported-agent list, and per-agent model overrides. Capsule reads it at startup and treats it as the source of truth for session spawning.
* **Initial agent selection** crosses from host to container as argv on `docker run`, not as a container-wide `JACKIN_AGENT` environment variable. The daemon uses it only to spawn the first tab.
* **Per-session agent identity** is set later by the daemon immediately before it spawns `/jackin/runtime/entrypoint.sh` for an agent pane. Shell panes get no `JACKIN_AGENT`.
* **Terminal input** crosses through the attach channel as raw bytes in `Input` frames. The daemon either consumes bytes that target Capsule UI (palette, prefix commands, tab chrome, dialog controls) or writes them to the active PTY unchanged.
* **Wheel input** is routed by focused-pane state. Mouse-enabled programs receive re-encoded pane-local SGR or xterm mouse events. Normal-screen panes that did not request mouse reporting use jackin' scrollback only when the grid has retained history, including rows preserved before normal-screen clear/redraw and top-anchored scroll-region movement; that same retained history is the only source for pane scroll chrome. When retained history is empty, wheel input stays local to jackin' so it cannot mutate the prompt as cursor keys, whether the pane was spawned as an agent or shell. Alternate-screen panes without mouse reporting receive cursor-key fallback, matching terminal emulator alternate-screen scroll behavior.
* **Terminal output** is read by a per-session PTY reader, parsed into that pane's `DamageGrid`, composed with Capsule chrome, and sent back to the attached client as raw ANSI bytes in `Output` frames.
* **Session inventory** is read from the daemon's in-memory session table and returned through the control channel as typed JSON. The host does not scrape rendered terminal text to learn which sessions exist.
* **Console previews** are read from the daemon's tab/pane snapshot. On same-kernel Docker hosts the host opens the bind-mounted Unix socket directly; on Docker Desktop, where the macOS host cannot connect to a Linux VM Unix socket, jackin' falls back to `docker exec ... /jackin/runtime/jackin-capsule snapshot` and reads the same JSON response through stdout.
* **Runtime setup inputs** are already present in mounted `/jackin/` state and environment selected by the host launcher. `jackin-capsule runtime-setup` copies or applies that state inside the container before the agent process replaces the entrypoint.

The full lifecycle for a normal attach looks like this:

```text
host: jackin load
  -> docker run -d ... <image> <initial-agent>
container: jackin-capsule PID 1
  -> bind /jackin/run/jackin.sock
  -> spawn initial PTY
  -> exec /jackin/runtime/entrypoint.sh inside that PTY
entrypoint:
  -> jackin-capsule runtime-setup
  -> exec selected agent runtime
host: docker exec -it <container> /jackin/runtime/jackin-capsule
client:
  -> connect /jackin/run/jackin.sock
  -> send Hello(rows, cols, spawn=None)
daemon:
  -> send Welcome
  -> render current tab/pane tree as Output frames
operator:
  -> keys become Input frames
daemon:
  -> Capsule UI consumes palette/prefix/dialog keys
  -> all other bytes go to focused PTY stdin
PTY reader:
  -> reads agent output
  -> updates DamageGrid screen
  -> render loop emits Output frames
```

Starting another agent session is the same attach flow with a spawn request:

```text
host: jackin hardline --new --agent codex
  -> docker exec -it <container> /jackin/runtime/jackin-capsule new codex
client:
  -> send Hello(rows, cols, spawn=Agent("codex"))
daemon:
  -> create new tab/pane
  -> set JACKIN_AGENT=codex only for that PTY
  -> run /jackin/runtime/entrypoint.sh
  -> focus the new tab
```

Reading state without attaching uses the control channel instead:

```text
host:
  -> open ~/.jackin/sockets/<container-name>/jackin.sock
  -> write len_be32 + {"type":"snapshot"}
daemon:
  -> read one request
  -> serialize ServerMsg::Snapshot
  -> write len_be32 + JSON
  -> close connection
host:
  -> render console preview / pane picker
```

On Docker Desktop, the host cannot open the Linux VM's Unix socket path directly even though the parent directory is bind-mounted. In that case <RepoFile path="crates/jackin-runtime/src/runtime/snapshot.rs">crates/jackin-runtime/src/runtime/snapshot.rs</RepoFile> falls back to `docker exec ... /jackin/runtime/jackin-capsule snapshot`; the in-container client still speaks the same control protocol to the same daemon socket, then prints the JSON response for the host to read.

### Failure and Security Boundaries [#failure-and-security-boundaries]

The socket is powerful because the attach channel can send bytes to the focused PTY. The current security model is therefore filesystem ownership:

* `/jackin/run` is created under the single `/jackin/` container-owned tree and chmodded `0700`.
* `/jackin/run/jackin.sock` is chmodded `0600`.
* The listener caps concurrent clients at 16 and drops excess connections so an in-container process sharing the agent uid cannot exhaust all attach slots.
* Malformed control requests are rejected and logged; the daemon does not block waiting forever for a reply it will never send.
* Oversized control and attach payloads are rejected before allocation beyond the 4 MiB cap.
* The host does not write into the socket path except by mounting the per-instance directory. Runtime communication is host-to-container or in-container; it does not mutate host config, host repos, or host credentials.

If the socket listener fails repeatedly, the accept loop backs off and then stops after repeated errors instead of spinning and flooding logs. Because the Capsule is PID 1, unrecoverable daemon failure naturally brings the role container down and lets the host path report a stopped or crashed instance rather than leaving an invisible half-attached terminal.

### Communication paths [#communication-paths]

The `jackin` CLI talks to the Capsule through a few deliberately narrow paths:

| Path               | Mechanism                                                                                       | Purpose                                                                                                                                                                                                                                                        |
| ------------------ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Initial launch     | `docker run -d ... <image> <agent>`                                                             | Starts `jackin-capsule` as PID 1. The trailing `<agent>` argv selects only the first tab; it is not exported as global container state.                                                                                                                        |
| Interactive attach | `docker exec -it <container> /jackin/runtime/jackin-capsule`                                    | Starts a second `jackin-capsule` process in attach client mode. That client connects to the PID 1 daemon over `/jackin/run/jackin.sock` and bridges the operator terminal.                                                                                     |
| New agent or shell | `docker exec -it <container> /jackin/runtime/jackin-capsule new [agent]`                        | The attach client sends a `Hello` frame with a spawn request. The daemon creates the new PTY and sets `JACKIN_AGENT` only for agent sessions.                                                                                                                  |
| Session inventory  | `jackin-capsule status`, direct host `UnixStream`, or `docker exec ... jackin-capsule snapshot` | Sends a typed control request and receives typed state. The CLI command prints a human-readable line format for older host paths; console snapshot code reads JSON directly, using `docker exec` when the host cannot connect to the bind-mounted Unix socket. |
| Runtime setup      | `/jackin/runtime/entrypoint.sh` calls `jackin-capsule runtime-setup`                            | Runs container-local setup immediately before the selected agent process is `exec`'d.                                                                                                                                                                          |

### Layout model [#layout-model]

Each tab owns a binary tree of panes. Any leaf can be split into two children (`HSplit` or `VSplit`) with a configurable ratio. The operator can build an arbitrary N×M grid by successive splits. The active pane receives input; all other panes continue running.

```
Tab
└── HSplit (ratio: 0.5)
    ├── Leaf → Claude session
    └── VSplit (ratio: 0.5)
        ├── Leaf → Codex session
        └── Leaf → Shell
```

### Virtual terminal emulator [#virtual-terminal-emulator]

Each pane owns one PTY and one `DamageGrid` (from the <RepoFile path="crates/jackin-term/src/grid.rs">crates/jackin-term/src/grid.rs</RepoFile> jackin-term crate). The grid feeds PTY bytes through a `vte::Parser` and maintains the cell grid, alternate screen, scrollback, cursor, and mode state, while a typed passthrough stream (`drain_passthrough()`) surfaces the OSC and extension sequences that need focused-pane forwarding. When the operator switches tabs, splits, or reattaches, the compositor re-renders the saved grid to the host terminal. Wide glyph continuation cells are skipped during replay so CJK and emoji output keeps the same column layout the agent drew.

### Render model [#render-model]

`jackin-capsule` is a re-emitting terminal multiplexer: it reads each pane's PTY into an owned jackin-term `DamageGrid`, then composes the operator's visible screen from those grids plus jackin' chrome. The current renderer has exactly one emit path. Every visible update is one `ratatui::Terminal::draw` of the full widget tree — status bar, pane bodies, borders, shared scrollbars, bottom chrome, dialogs, text selection, and transient banners — encoded by <RepoFile path="crates/jackin-capsule/src/tui/socket_backend.rs">crates/jackin-capsule/src/tui/socket\_backend.rs</RepoFile> and written by <RepoFile path="crates/jackin-capsule/src/client_writer.rs">crates/jackin-capsule/src/client\_writer.rs</RepoFile> inside synchronized-output brackets.

The one-path rule exists because the old mixed renderer had three writers sharing one client-screen baseline: Ratatui frames, a focused-pane dirty-patch tier, and raw appends for chrome, cursor/mode assertions, hyperlinks, and banners. That shape made stale cells structural: a glyph that changed and changed back between Ratatui frames could be skipped forever because the patch path had already mutated the real terminal while Ratatui's previous buffer never learned about it. It also made scrollback and focus changes fragile because a handler could mutate state without requesting the exact repaint tier that happened to refresh both body and footer. The shipped renderer removes the class, not just the observed failures: handlers mutate state and record an invalidation, the render loop composes when `frame_generation` moves, and `ClientWriter` is the only object that can reach the attach socket.

Damage tracking still matters, but only as a "did anything change?" signal and as the observation API for tests and future tooling. `DamageGrid` records dirty rows at mutation time, owns primary/alternate/scrollback rows, cursor state, modes, grapheme cells, and typed passthrough events, and is the only terminal model in the daemon. The compositor no longer serializes `GridPatch` rows directly to the client. Pane bodies borrow `GridView` data during the Ratatui draw, and the buffer diff is the only "what changed on the client?" computation. The [Capsule Terminal Model](/reference/capsule/terminal-model/) page records why the old `vt100` fork and the temporary patch tier were retired.

A real screen erase (`\x1b[2J`) is intentionally rare: first attach and resize only. Tab switches, focus swaps, scrollback movement, dialog open/close, selection changes, session exit banners, and repaint convergence all redraw in place. `ClientWriter` wraps every non-empty frame in `\x1b[?2026h` / `\x1b[?2026l`, so the outer terminal applies the frame atomically instead of showing a partially repainted intermediate state. Out-of-band sequences such as clipboard writes, pointer-shape hints, allowed OSC passthrough, and keyboard-protocol passthrough are queued and flushed at frame boundaries rather than interleaved inside a frame.

The frame model carries more than cells. `SocketBackend` reconciles cursor visibility, cursor shape, bracketed paste, application cursor keys, kitty keyboard level, and hyperlink regions from the focused pane's grid on every frame. Focus swap is therefore a normal render, not a special reset path, and the cursor stays hidden while browsing scrollback because the focused pane's view state says it should. With `JACKIN_DEBUG=1`, render logs include invalidation reason, wipe policy, generation, emitted bytes, and compose timing so frame behavior can be reconstructed from an operator debug run.

The correctness trade-off is explicit. The deleted dirty-patch tier was cheaper on the wire for a focused 80x24 stream, but it made the physical client terminal a second untracked model. The full-widget-tree path measured well below one frame budget and is mechanically checked by the echo-back conformance harness: each composed frame is fed into a second `DamageGrid` acting as the client terminal, then the test asserts that the client screen equals each pane model inside its pane rectangle. If a future pathological terminal size needs optimization, the allowed escape hatch is to skip re-rendering clean pane widgets into the Ratatui buffer; it must not add a second emit path or writer.

### Status bar [#status-bar]

The top two rows of the host terminal are owned by `jackin-capsule` and never yielded to agent sessions. They render:

* **`jackin'` brand pill** — bright-green background, near-black text. Always visible. When an operator shares their screen, others can immediately see the container is running inside jackin'.
* **Session tabs** — one tab per session. Active tab is bold white text on a lifted graphite background, separate from the brand green, with the white underline as the primary active cue. Inactive tabs are grey. Mouse-clickable tabs lift their background on hover. Tabs show a state glyph for the session roll-up. The blocked attention glyph is latched: incidental PTY output does not clear it, and explicit operator keyboard input to that pane does.
* **Menu button** — the right side of row 0 keeps a padded, clickable `☰Menu` button. It uses blue chrome rather than the green brand/focus color, lifts on hover, and switches the pointer cursor where supported. When prefix mode is enabled and awaiting the next key, the button changes to `prefix…` so the operator can see that Capsule has consumed the prefix byte and is waiting for the command key.

Capsule always reserves the bottom terminal row for the white session context bar. The right side shows the short jackin instance ID from `JACKIN_INSTANCE_ID`, such as `spamcw91` for `jk-spamcw91-jackin-thearchitect`. The launcher injects both `JACKIN_CONTAINER_NAME` and `JACKIN_INSTANCE_ID` before the capsule daemon starts, so the first frame can render the instance ID without waiting for GitHub, branch polling, shell startup, or a later hostname update. The left side is reserved for branch or pull request context only: when the workspace workdir's current branch has an open GitHub pull request, it shows `PR #<number> · <title>`. While `gh` is still resolving an open pull request for a branch, it shows `Resolving PR · <branch>` instead of pretending there is no pull request. On a non-`main` / non-`master` branch without an open PR, the same row shows `Branch · <branch>` on the left. On `main`, `master`, or a directory without a recognizable branch, the left side stays empty and only the instance ID remains visible. The PR or branch portion opens a GitHub context modal with branch, URL, title, best-effort `gh pr checks` status, and an `Enter` hint for copying the PR URL when a pull request exists. Opening that modal schedules a fresh background `gh` lookup for the current branch and `HEAD`, bypassing a recent cached "no PR" answer and still trying the lookup if startup missed `gh` availability. The empty left side on the default branch is not a GitHub-context target. The GitHub URL value is the emphasized clickable copy target. The right side opens Debug info with the full container name, role, active pane's agent, current workdir, run id when available, and diagnostics path when available. Enter copies the first copyable value in the shared row order (`Run ID` whenever present), and clicking a copyable value copies that row. All click targets switch to a pointer cursor on supported terminals and lift their color while hovered. The daemon watches the container-side Git metadata directory for `HEAD` changes, so branch switches invalidate stale PR context immediately without a hot polling loop. A once-per-second poll remains as a fallback for missed filesystem events and `HEAD` movement that does not rewrite `HEAD`. Background `gh` PR/check lookups stay cached for the current branch plus `HEAD` and refresh on a conservative cadence; switching to `main` or `master` clears the branch/PR detail while keeping the always-on instance ID.

Most Capsule dialogs render through the same full-frame overlay path. That path paints an opaque solid-black backdrop over the content area and then draws the active dialog on top: pane bodies, pane borders, active-pane focus chrome, and scrollbars are hidden behind the fill, not dimmed. (Dimming the live content read as unstable, so the modal hides it outright.) The dialog footer hint renders above the bottom status row with one blank spacer row between them, so modal key hints stay in the bottom chrome area instead of inside or next to the dialog.

Debug info is the status-preserving exception and follows the shared dialog contract in [Dialogs & Modals](/reference/tui/dialogs/#debug-info-dialog-contract): the top status/tab chrome and bottom context row remain visible, the dialog is centered in the content area between those reserved rows, and the copy/hover/scroll behavior comes from `jackin_tui::components::ContainerInfoState`.

The status bar is drawn after hiding the cursor, and the cursor is restored after the draw. It never causes the pane content to repaint unnecessarily.

### Input routing [#input-routing]

```
Key press arrives
  │
  ├─ Row 0 mouse click → tab switch
  ├─ Bottom context-row click → GitHub context on the left, Debug info on the right
  ├─ Direct palette key (default `Ctrl+\`, override via JACKIN_PALETTE_KEY) → command palette
  ├─ Prefix key (Ctrl+B by default when JACKIN_PREFIX is set) → tmux-style prefix-command dispatcher (Space/`:` palette, `c` new tab, `d` detach, `Ctrl+L` clear pane, `"` / `%` splits, `n` / `p` tabs, etc.)
  ├─ Prefix focus/resize commands and Alt+Shift+Arrow resize commands → multiplexer action
  ├─ Command palette open → palette key handler
  └─ Everything else → active pane PTY stdin
```

The palette key (and, when enabled, the prefix-key state machine) are the only keystrokes the multiplexer intercepts from the input stream in normal mode. The default `Ctrl+\` (`0x1C`) is picked because raw-mode terminals never emit it as content and no agent uses it as an editing key; the earlier `Ctrl+J` default collided with the literal LF byte that multi-line editors use as a line continuation, so `Ctrl+J` is only available as an opt-in via `JACKIN_PALETTE_KEY=C-j`. Plain `Ctrl+L` is forwarded unmodified; the clear path owned by jackin' is explicit through the palette's **Clear pane** action or prefix then `Ctrl+L`. That command clears the scrollback the pane's `DamageGrid` holds, sends form-feed to the pane so the shell or TUI redraws its own visible screen, and never uses RIS (`ESC c`) because a reset would disrupt pane modes. The close path is scoped by the active tab's pane count: a single-pane tab renders **Close tab** and routes straight to confirmation, while split tabs render **Close** and push the pane-vs-tab target picker. The top-level **Exit** command opens an &#x2A;*Exit?** confirmation; confirming terminates every live pane, lets PID 1 exit after the session set is empty, and hands control back to the host finalizer so the normal cleanup/eject path removes the role container, DinD sidecar, certs volume, and network. Detach remains explicit through prefix `d` or the attach protocol's `Detach` frame and keeps sessions running. Everything else — including all agent-specific shortcuts, bracketed paste, mouse events, and extended key sequences — is forwarded unmodified to the active PTY.

Dialog dispatch receives normalized keys, not raw terminal protocol details. When a focused agent enables kitty / CSI-u keyboard reporting, the input parser converts unmodified arrow keys and Escape back to the legacy byte forms the dialog handlers match, and it suppresses release events. That keeps `Esc` as "pop one dialog frame" even after an embedded TUI has enabled progressive keyboard reporting.

<Aside type="caution">
  The agent's terminal experience is the highest-priority constraint. No multiplexer feature may intercept, transform, or drop input that the agent should receive. See the [Multiplexer Design Rules](/reference/capsule/multiplexer-design-rules/) for the full passthrough contract.
</Aside>

### Agent discovery [#agent-discovery]

Available runtimes are declared in `/jackin/run/agent.toml`, which is generated from the validated role manifest at launch time. The daemon reads the ordered `agents` list at startup to populate the agent picker in the command palette and to validate `jackin-capsule new <agent>` requests; model overrides live in the same file's `[models]` table.

### OSC passthrough opt-outs [#osc-passthrough-opt-outs]

Modern terminals interpret OSC escape sequences agents emit for window titles, clipboard writes, desktop notifications, and hyperlinks. By default jackin-capsule forwards each family from the focused pane to the operator's outer terminal so the agent's UI works the same as it would running directly on the host. Operators running an untrusted role can disable any family per-container with these environment variables (set on the host before `jackin load` so they reach the derived image's runtime):

| Env var                | Disables when set to `deny` / `off` / `no`       |
| ---------------------- | ------------------------------------------------ |
| `JACKIN_OSC52`         | OSC 52 clipboard writes (agent → host clipboard) |
| `JACKIN_OSC_TITLE`     | OSC 0 / 1 / 2 window-title and icon-name updates |
| `JACKIN_OSC_NOTIFY`    | OSC 9 desktop notifications                      |
| `JACKIN_OSC_HYPERLINK` | OSC 8 clickable hyperlinks                       |

OSC 7 (current working directory) is never forwarded — it would tell the operator's host terminal to track the *container's* cwd, which has no meaning outside the container; jackin-capsule captures it locally for the pane title and stops. OSC 8 forwarding additionally rejects any URI whose scheme is not `http`, `https`, or `mailto` (so a compromised agent cannot ship a `javascript:` or `file://` hyperlink that would script the operator's host terminal on click).

The multiplexer also emits its own OSC 2 title from the compositor. That title is not pane content: it is derived from `/jackin/run/agent.toml`'s workdir and the same filtered GitHub branch / pull request context that feeds the bottom context bar, then re-emitted only when the value changes. This keeps Ghostty-style tab and window lists anchored to the workspace plus pull request title or non-default branch even when a focused pane briefly forwards its own shell title.

### Pointer shape feedback [#pointer-shape-feedback]

The attach client enables SGR any-event mouse tracking on the outer terminal. When the daemon sees passive pointer motion, it emits OSC 22 pointer-shape hints for interactive chrome when the client reports a real terminal identity through `TERM_PROGRAM` or a known OSC 22-capable `TERM` family. Clearly non-interactive terminals (`TERM=dumb`, `TERM=linux`), generic PTYs that only report `xterm-256color`, and known terminals that mishandle the sequence stay on the default pointer:

* `pointer` over clickable chrome owned by jackin', such as tabs, the top menu button, the bottom-row GitHub and Debug info entry points, and clickable dialog values such as Container ID, Run ID, Diagnostics log, and GitHub URL copy targets.
* `text` over pane bodies where mouse selection owned by jackin' is available because the focused program did not request mouse reporting.
* `ew-resize` / `ns-resize` over split borders and `grabbing` during an active resize drag.
* `default` everywhere else, and again during attach cleanup.

Terminals that do not implement OSC 22 should ignore the sequence, while terminals that do implement it get the same cursor affordance the web UI uses for clickable elements. The visual behavior is still an enhancement rather than a dependency for mouse input or copy/select behavior.

Clickable Capsule chrome also repaints on hover when passive mouse motion identifies a known target. Tabs, the top menu button, the bottom GitHub context target, the bottom Debug info target, and dialog copy values use a slightly brighter background or link color so clickability is visible even when pointer-shape escapes are unsupported.

The policy is read once per session at spawn time; an agent that exports the env var after start does not flip the gate for that session. Set the values on the host, before `jackin load`, to ensure the derived image carries them through to the multiplexer's environment.

## Distribution and versioning [#distribution-and-versioning]

`jackin-capsule` is versioned identically to the `jackin` CLI. Both binaries use the shared <RepoFile path="crates/jackin-build-meta/src/lib.rs">jackin-build-meta</RepoFile> build-script helper: local and pull-request builds fall back to `{cargo_pkg_version}+{7-char-git-sha}`, which is currently the `0.6.0-dev+<sha>` shape, while the main-branch preview workflow injects the published `0.6.0-preview.<count>+<sha>` string through `JACKIN_VERSION_OVERRIDE`. The host CLI, `jackin-role`, and `jackin-capsule --version` output must match the version string embedded in the artifact being tested or published.

### How jackin' acquires the binary [#how-jackin-acquires-the-binary]

`ensure_available` in <RepoFile path="crates/jackin-image/src/capsule_binary.rs">crates/jackin-image/src/capsule\_binary.rs</RepoFile> resolves the binary in priority order:

1. **`JACKIN_CAPSULE_BIN=/path/to/binary` (env var set)** — jackin' uses that path directly. No cache lookup, no download. The binary can come from anywhere: a local build, a CI artifact, a closed-source build. jackin' validates only that the file exists and is executable.

2. **Cache hit** — if `~/.jackin/cache/jackin-capsule/<version>/linux-<arch>/jackin-capsule` exists and is executable, it is used immediately.

3. **Packaged binary** — Homebrew formulae install the matching Linux `jackin-capsule` resource under the formula's `libexec/jackin-capsule/linux-<arch>/jackin-capsule` path. If that binary exists and passes the same version check, jackin' uses it directly.

4. **Download** — on a cache and package miss, a `jackin-capsule-*.tar.gz` archive and its `.sha256` companion are downloaded from GitHub Releases: the rolling `preview` tag for `-dev` and `-preview.*` versions, or the versioned `v<version>` tag for stable releases. The archive hash must match before jackin' extracts the top-level `jackin-capsule` binary into the cache. On Linux the extracted binary is also verified by executing `jackin-capsule --version` and asserting the output matches the expected version string.

jackin' itself has no knowledge of how to build `jackin-capsule` from source. Build logic lives exclusively in the developer tool described below.

### Building for local development [#building-for-local-development]

Use the `build-jackin-capsule` binary (defined in <RepoFile path="crates/jackin/src/bin/build_jackin_capsule.rs">crates/jackin/src/bin/build\_jackin\_capsule.rs</RepoFile>) to compile `jackin-capsule` for Linux and populate the cache. It uses `cargo-zigbuild` with Zig as the linker — no Docker required — and targets a glibc 2.17 floor to match CI:

* `arm64` → `aarch64-unknown-linux-gnu.2.17`
* `amd64` → `x86_64-unknown-linux-gnu.2.17`

**Option A — one shot (recommended for PR verification):**

```sh
eval "$(cargo run --bin build-jackin-capsule -- --export)"
```

This builds the binary, caches it under `~/.jackin/cache/jackin-capsule/`, and sets `JACKIN_CAPSULE_BIN` in the current shell so `jackin load` uses it directly — bypassing the cache lookup entirely.

**Option B — two steps (useful when running multiple `jackin load` invocations):**

```sh
cargo run --bin build-jackin-capsule
# prints: [build] to use: export JACKIN_CAPSULE_BIN=/path/to/cached/binary
export JACKIN_CAPSULE_BIN=/path/to/cached/binary
```

The binary is written to the standard cache path and subsequent `jackin load` invocations in the same shell reuse it without rebuilding.

**Flags:**

| Flag                  | Description                                                                                                                                                                                                                                                                                             |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--arch arm64\|amd64` | Target architecture (default: current host's container arch)                                                                                                                                                                                                                                            |
| `--profile debug`     | Build with the `capsule-debug` Cargo profile: symbols retained, line tables included. Backtraces from `Backtrace::force_capture()` resolve to function names, file paths, and line numbers instead of `<unknown>`. Output binary is named `jackin-capsule-debug`. The lean release binary is unchanged. |
| `--debug`             | Convenience alias for `--profile debug`.                                                                                                                                                                                                                                                                |
| `--export`            | Print only `export JACKIN_CAPSULE_BIN=<path>` for `eval`                                                                                                                                                                                                                                                |

Prerequisites: `zig` and `cargo-zigbuild` must be installed. Both are declared in <RepoFile path="mise.toml">mise.toml</RepoFile>; run `mise install zig cargo:cargo-zigbuild` if missing.

First build takes 2–3 minutes; subsequent builds are incremental via cargo's dependency cache.

**Debug builds for crash triage.** When a container crashes with an exit-101 panic and the backtrace in `multiplexer.log` shows `<unknown>` frames, use the canonical [Debugging Capsule Crashes](/reference/capsule/session-lifecycle/#debugging-capsule-crashes-symbolicated-build) flow. That page owns the full symbolicated-build recipe so the build command, `RUST_BACKTRACE=full` launch, run-id lookup, and `capsule_log` triage steps do not drift across two copies.

### Derived image build [#derived-image-build]

<RepoFile path="crates/jackin-image/src/derived_image.rs">crates/jackin-image/src/derived\_image.rs</RepoFile> copies the resolved binary into the Docker build context so `docker build` does not need network access at image-build time. The `COPY .jackin-runtime/jackin-capsule /jackin/runtime/jackin-capsule` instruction is emitted only when the binary path is available; when it is not (e.g. in unit tests), the instruction is omitted and the image falls back to whatever `/jackin/runtime/jackin-capsule` the base image provides.

## Relationship to the rest of jackin' [#relationship-to-the-rest-of-jackin]

| Component                                                                                                                   | Relationship                                                                                                                  |
| --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| <RepoFile path="crates/jackin-image/src/capsule_binary.rs">crates/jackin-image/src/capsule\_binary.rs</RepoFile>            | Resolves the binary path: `JACKIN_CAPSULE_BIN` override → cache hit → download from GitHub Releases                           |
| <RepoFile path="crates/jackin/src/bin/build_jackin_capsule.rs">crates/jackin/src/bin/build\_jackin\_capsule.rs</RepoFile>   | Developer tool: `cargo run --bin build-jackin-capsule` — builds via `cargo-zigbuild` and populates the cache                  |
| <RepoFile path="crates/jackin-image/src/derived_image.rs">crates/jackin-image/src/derived\_image.rs</RepoFile>              | Copies binary into derived image build context                                                                                |
| <RepoFile path="crates/jackin-capsule/src/runtime_setup.rs">crates/jackin-capsule/src/runtime\_setup.rs</RepoFile>          | Implements `jackin-capsule runtime-setup`, the Rust replacement for deterministic entrypoint shell setup                      |
| <RepoFile path="crates/jackin-runtime/src/runtime/launch.rs">crates/jackin-runtime/src/runtime/launch.rs</RepoFile>         | Adds the per-instance socket directory mount to `docker run`; connects via `docker exec jackin-capsule`                       |
| <RepoFile path="crates/jackin-runtime/src/runtime/attach.rs">crates/jackin-runtime/src/runtime/attach.rs</RepoFile>         | Queries socket for session list; spawns new sessions via `jackin-capsule new`                                                 |
| <RepoFile path="crates/jackin-runtime/src/isolation/finalize.rs">crates/jackin-runtime/src/isolation/finalize.rs</RepoFile> | Queries socket to detect live sessions before cleanup                                                                         |
| [Agent runtime status authority roadmap item](/reference/roadmap/agent-runtime-status/)                                     | Defines the in-container state authority and consumes agent-state events from the socket once the event stream is implemented |
| [jackin' daemon roadmap item](/reference/roadmap/jackin-daemon/)                                                            | The daemon will subscribe to each container's socket and maintain a cross-instance session index                              |
| [Multiplexer Design Rules](/reference/capsule/multiplexer-design-rules/)                                                    | Passthrough contract, input routing rules, verification checklist                                                             |

## See also [#see-also]

* [Multiplexer Design Rules](/reference/capsule/multiplexer-design-rules/) — the non-negotiable passthrough contract and per-feature verification checklist
* [jackin-capsule roadmap item](/reference/roadmap/jackin-capsule/) — original problem statement, Herdr evaluation, phased implementation plan
* [Agent Orchestrator Research](/reference/roadmap/agent-orchestrator-research/) — comparative analysis of Herdr and other tools that informed the design
