Host Affordance Bridge — Design Rationale
This page is the design-rationale and architecture-decision record for the Host Affordance Bridge roadmap item: why the feature exists, what alternatives were rejected, the protocol/architecture shape chosen, the security model, and the phased plan that produced the current implementation. The roadmap page owns current status and remaining work; this page explains why the design looks the way it does and is not updated as implementation status changes. See also the Host Affordance Bridge Preflight for the Phase 0 measurements that grounded this design.
Problem
jackin❯ runs agent CLIs inside isolated Docker containers and multiplexes them through jackin-capsule. That isolation is the product boundary, but it currently breaks several local-terminal expectations: when the operator has an image in the host clipboard and pastes into Claude Code, Codex, or another multimodal CLI, the agent should receive an image attachment or an image file path it can read; when the operator sees a PR, CI run, or ordinary URL, they should be able to open it in the host browser; when an agent creates a file inside the container, the operator should be able to intentionally export it to the host; when a later drag/drop or file-paste feature exists, the staged file should appear inside the container. Today those actions work naturally when the agent runs directly on the host, but not reliably through jackin❯ because the process that can read host clipboard, open the host browser, receive desktop drag/drop information, or write to host Downloads/Desktop is not the process receiving input inside the container.
The visible symptom is that a paste often becomes a path, nothing, or ordinary text instead of an image. The structural problem is deeper: jackin❯ currently treats attach input as a terminal byte stream. Host clipboard image data is not guaranteed to exist in that stream. It usually lives behind the host OS clipboard APIs, terminal-app paste events, or terminal-specific behavior before any bytes reach stdin.
This is not just a missing convenience feature. It is a host/container parity gap: jackin❯ should make an agent running inside a container feel like the same agent running on the host unless isolation intentionally changes behavior. Clipboard image paste, GitHub/CI opening, clickable URL behavior, and container-file export are places where the boundary is visible today.
Root Cause
The architecture put the interactive attach client inside the container:
Ghostty / terminal on host
-> docker exec -it <container> /jackin/runtime/jackin-capsule
-> /jackin/run/jackin.sock
-> Capsule daemon
-> focused agent PTYThe host terminal can see Cmd+V, Ctrl+V, drag/drop events, file promises, and the host clipboard. The in-container jackin-capsule attach client cannot. It only reads whatever bytes Docker forwards to the pseudo-terminal. For text paste, that is usually enough. For image paste, it is not.
That means a symptom-layer patch inside the Capsule input parser cannot be correct by itself. Capsule can recognize bytes that arrive, but it cannot recover image bytes that never crossed the host/container boundary. The root-cause fix is to move image-aware input capture to a host-owned component and then send a typed payload across jackin❯'s supported host/container boundary.
Research Findings
Host CLIs
The public Codex CLI documentation says Codex supports image inputs in two ways: paste images into the interactive composer, or pass image file paths with --image / -i. It does not document the internal paste implementation, but the architecture implication is clear: a local Codex process has access to the host clipboard and local image files before it constructs the model request. See Codex CLI features — Image inputs.
Claude Code exposes similar behavior in practice: when run directly on the host, it detects an image in the clipboard and inserts an image placeholder into its prompt composer. That does not prove a public protocol jackin❯ can rely on; it proves the host-local process has a capability the in-container process lacks.
herdr
herdr is the closest public reference because it is a Rust terminal-native agent multiplexer. It is not embedded in jackin❯ because herdr is AGPL-3.0 while jackin❯ is Apache-2.0, but its design is directly informative.
The relevant herdr shape:
- The host-side client watches terminal input for a paste trigger such as
Ctrl+Vor an empty bracketed-paste sequence. - On that trigger, it asks the host platform for an image clipboard payload.
- macOS reads the pasteboard through AppleScript by coercing clipboard data to PNG.
- Linux reads image MIME types through
wl-pasteon Wayland orxclipon X11. - The client validates and caps the image payload, then sends a typed
ClipboardImage { extension, data }message over herdr's client/server protocol. - The server stages the image to a private temp file and routes the staged path as a paste event to the active pane.
The files that matter in herdr are src/client/mod.rs, src/platform/macos.rs, src/platform/linux.rs, src/server/clipboard_image.rs, src/server/headless.rs, and src/server/terminal_attach.rs. jackin❯ borrowed the architecture and validation ideas, not the code.
Ghostty and terminal behavior
Ghostty already supports terminal graphics display through the Kitty graphics protocol, and it can paste file-backed images in some flows, but there is an explicit Ghostty discussion showing a gap for screenshots captured directly to the macOS clipboard: ghostty-org/ghostty discussion #10478. That distinction matters. A file copied from Finder can become a pasteable path or terminal image indicator; a screenshot copied directly to the pasteboard may never become a path or byte stream for the child process.
This confirmed the jackin❯ design should not assume "image paste" is one terminal protocol. It has to handle at least three host clipboard shapes:
| Clipboard shape | Example | Correct jackin❯ behavior |
|---|---|---|
| Raw image data | macOS screenshot copied with Shift+Cmd+Ctrl+4, image copied from a browser | Read host clipboard bytes, validate, stage inside the container, paste readable container path. |
| File promise / file URL / copied file | Screenshot saved to disk then copied in Finder, image copied from a file manager | Resolve the host file if permitted, copy or bind/stage it into the container, paste readable container path. |
| Plain text path | Operator copied /path/to/image.png text | Treat as ordinary text unless an explicit image-paste command says "stage this path." |
Existing jackin❯ clipboard surface
jackin-capsule already had clipboard behavior that had to remain intact:
- Pane programs can write text to the host clipboard through OSC 52 passthrough only when the operator opted in with
JACKIN_OSC52=allow. - jackin-owned actions such as text selection, double-click word copy, Debug info copyable values, and launch-failure copy targets emit OSC 52 from the multiplexer.
- Those copy actions render visible feedback, such as the
Selection copiedtoast, because terminals may accept, deny, or silently ignore clipboard writes. - Bracketed paste from the host terminal is mirrored from the focused pane and forwarded as ordinary input bytes.
The screenshot-driven lesson is that image paste had to join this surface deliberately. It should not be a special case hidden in the input parser, and it should not replace existing OSC 52 text copy behavior. The correct model is a clipboard boundary with explicit direction, ownership, and permission:
| Direction | Mechanism | Boundary owner |
|---|---|---|
| Pane writes text to host clipboard | OSC 52 when JACKIN_OSC52=allow | Capsule output policy and host terminal policy. |
| jackin❯ UI writes text to host clipboard | OSC 52, with visible copy feedback | Capsule-owned output path and host terminal policy. |
| Host text paste enters focused pane | Terminal byte stream, bracketed-paste-aware | Host terminal plus attach client. |
| Host image paste enters focused pane | Host clipboard reader, typed ClipboardImage frame, container staging, pasted container path | Host attach client plus Capsule staging. |
| Agent reads host clipboard directly | Not allowed | No implementation path. |
This keeps copy-out, text paste-in, and image paste-in separate. That separation is a correctness property, not just an implementation preference: text copy is terminal-mediated, text paste is byte-stream-mediated, and image paste requires a host API read plus a file transfer into the container.
Adjacent host affordance: opening links
The same host/container boundary appears with hyperlinks. Capsule already forwards safe OSC 8 hyperlinks (http, https, and mailto) to the outer terminal, and it also renders jackin-owned hyperlink regions for chrome and dialogs. That is correct but incomplete: not every terminal exposes a convenient clickable-link gesture, and some visible links are plain text rather than OSC 8 regions.
The design goal was a host attach client that owns an explicit "open link under cursor" affordance:
- The operator performs an explicit gesture, such as
Cmd-click on macOS if the terminal exposes that modifier to the program,Ctrl-click where that is the portable terminal modifier, or a command palette action such as "Open link under cursor". - The host attach client asks Capsule which safe link is under the cursor, or derives the visible URL from the current pane snapshot using the same URL-token semantics as double-click selection.
- The host attach client opens the URL with the host OS browser action (
openon macOS,xdg-openor a configured opener on Linux). - Capsule renders a compact feedback cue only if the action fails or is blocked by policy.
This had to be an explicit host action, not automatic browser opening when an agent prints a link. A compromised or noisy agent can emit unlimited OSC 8 hyperlinks; it must not be able to launch host browser windows without a user gesture.
Cmd-click is desirable on macOS, but it is a terminal capability question, not an assumption. Some terminals consume Cmd-click for their own hyperlink handling and never send it to the child program. Phase 0 measured what Ghostty and the other supported terminals actually send for Cmd-click, Option-click, and Ctrl-click (see the preflight page). Since Cmd never reached the host attach client in that measurement, jackin❯ provides the capability through a command palette action and the modifier sequences the terminal does expose.
The design covers two visible agent experiences:
| Experience | Examples | Link-open behavior |
|---|---|---|
| Plain CLI transcript | Codex-style output, shell output, logs, command results | Scan visible row text and scrollback snapshots for URL-shaped tokens; when the modifier is held and the cursor is over a safe URL, draw a subtle hover highlight and open on modified click. |
| Rich TUI / alternate-screen mode | Claude Code, Amp, Grok, and other TUIs that render panels, mouse targets, and OSC 8 links | Prefer OSC 8 hyperlink regions and jackin-known chrome link regions; fall back to visible URL-token detection on the current rendered grid. Normal mouse input continues to the TUI. Only an explicit link-open gesture on a safe URL is intercepted. |
| jackin-owned chrome and dialogs | GitHub context dialog, Debug info dialog, bottom PR target, launch-failure popup | Use first-class actions because jackin❯ already owns the URL and click target. |
The hover highlight is an overlay in Capsule's compositor, not bytes injected into the pane. If the focused TUI has mouse reporting enabled, ordinary motion, clicks, and drags still belong to the TUI. The link-open path is active only while jackin❯ can positively identify the operator's modifier or while an explicit command palette action is running. If the terminal cannot report Cmd hover state, the correct result is "command palette action works, modifier hover is unavailable in that terminal", not guessing from unmodified mouse movement and stealing rich-TUI input.
The GitHub context dialog was the canonical first chrome target. It already displays branch, PR number, PR title, GitHub URL, and CI status, so the host-affordance bridge added separate actions for "Open PR in browser" and "Open CI in browser"; when CI is failing, the CI action prefers the most specific failing check/run URL available rather than the generic repository Actions page.
This required new data in Capsule. The prior PR context lookup only asked gh pr list for number, title, url, and isDraft, then asked gh pr checks for bucket counts. V1 extended that state with check/run URLs.
tmux and OSC 52
tmux's clipboard integration is mostly about text clipboard synchronization. Its wiki describes OSC 52 and external clipboard tools as "blunt" and inconsistently supported, and calls out the security problem where allowing arbitrary applications to set the outside clipboard means anything that can write to a pane can affect the system clipboard. See the tmux clipboard wiki.
OSC 52 is useful for jackin❯ text copy out of the container and maybe for text clipboard reads in terminals that explicitly support queries. It is not a correct foundation for image paste. Many terminals do not support reading the clipboard through OSC 52; where reads exist, they typically require user consent; and the payload model is text-oriented rather than "give the remote program the host's screenshot as a file it can read."
Kitty graphics
The Kitty terminal graphics protocol lets a terminal application display raster images in a compatible terminal. That is the opposite direction from this feature. It answers "how can a program draw an image in Ghostty/kitty?" not "how can a program inside a Docker container receive an image from the host clipboard?" jackin❯ may eventually need Kitty graphics passthrough for visual previews, but it is not the clipboard image bridge.
Remote-terminal analogs
Remote SSH tools solve the same class of problem with an out-of-band upload: read the local clipboard, write an image file on the remote machine, then paste or copy the remote path. clipssh is a concise example of that model. jackin❯'s container boundary is not SSH, but the correct abstraction is the same: host image bytes must be deliberately transferred to the environment where the agent can read them, and the thing pasted into the agent should be the destination path.
Ghostty has an open SSH-image-paste discussion that describes the same desired user flow: detect a local clipboard image, upload it to the remote session, and insert the remote path so tools like Claude Code can read it. The proposed path there is SCP over an existing SSH control socket. jackin❯ does not need SSH for local Docker, but the product invariant is identical: paste/upload creates a destination-local file path, not a host-only path. See ghostty-org/ghostty discussion #10517.
Link handling references
WezTerm has a first-class OpenLinkAtMouseCursor action that can be bound to Ctrl-click, plus hyperlink_rules that turn visible text patterns into clickable links. This supports jackin❯'s planned split between explicit OSC 8 regions and implicit URL detection. See WezTerm OpenLinkAtMouseCursor and WezTerm hyperlink_rules.
xterm.js documents the common safety rule for link activation: terminals often require a modifier, usually Ctrl or Cmd on macOS, to avoid accidental window opening. It also calls out hover URL display as a safety feature, especially for OSC 8 links where the displayed text can differ from the actual target. jackin❯ follows that model: show enough target information on hover/debug surfaces, but do not log full query strings by default. See xterm.js Link Handling.
Rio's hyperlink hints are useful because they make modifier keys configurable and default to Command on macOS and Alt elsewhere. That argues against hard-coding Cmd as the only jackin❯ link-open gesture. jackin❯ prefers platform defaults but keeps a configurable modifier/open command path. See Rio Hyperlinks.
xterm mouse protocols can report Shift, Meta, and Control modifiers with mouse events, but the xterm documentation also notes that Shift and Control are often unavailable because terminals reserve them for selection or menus. That confirmed Phase 0 had to measure the actual terminal, especially Ghostty on macOS, instead of assuming modifier-hover would be visible to the attach client. See xterm control sequences — mouse tracking.
Shell integration and file transfer references
iTerm2's shell integration can download a file referenced in remote output and can upload a dragged Finder file to the remote host with SCP when Option is held. This is not a direct implementation target for jackin❯, but it proves the UX class is established: terminal chrome can turn a visible remote/container path or host drag/drop into an explicit transfer action. jackin❯'s equivalent is typed Capsule staging under /jackin/run/clipboard/ or a future /jackin/run/uploads/, not ad hoc shell text injection. See iTerm2 shell integration.
WezTerm shell integration uses OSC 7, OSC 133, and OSC 1337 to mark working directories, prompt/input/output zones, and pane state. jackin❯ already blocks OSC 7 passthrough to avoid host cwd pollution, but the idea of prompt/output zones is still useful: when shell sessions emit OSC 133, Capsule can use those zones to make transcript-mode link hover, command-output selection, and future "open artifact from last command" more precise. See WezTerm shell integration.
Kitty's clipboard kitten can read and write arbitrary clipboard MIME types, including raster images, and can work over SSH with permission prompts. That is a useful optional backend/protocol reference, but it should not become jackin❯'s primary path because it depends on Kitty-compatible terminal support and terminal-level clipboard permissions. The primary path remains a host attach client with platform readers. See Kitty clipboard kitten.
Kitty's remote-file kitten supports opening, editing, and downloading files from a remote SSH session. The terminal can use hyperlinks from commands such as ls --hyperlink=auto, then a modified click chooses an action. This is the closest UX precedent for jackin❯'s container-file export path: visible file path plus explicit operator action plus host-side destination. See Kitty remote files.
Kitty's transfer kitten is a more protocol-shaped precedent: it can copy files in either direction over the TTY device, including remote-to-local download and local-to-remote upload. jackin❯ does not depend on Kitty's implementation, but it validates a typed bidirectional file-transfer capability as a terminal-adjacent primitive. See kitten-transfer manual.
iTerm2's utilities include it2dl, which downloads a remote file into the local Downloads folder. This reinforces the correct host destination model for jackin❯: a predictable operator-owned download location with visible feedback, not silent writes to arbitrary host paths. See iTerm2 utilities.
Decision
The feature was implemented as a host-owned attach client with typed host-affordance frames, starting with clipboard-image frames and explicit host-link-open requests, not as a persistent host daemon in the first design.
The chosen data flow is:
Ghostty / terminal on host
-> jackin host attach client
-> read host clipboard on explicit paste trigger
-> validate and cap image bytes
-> Capsule attach protocol ClipboardImage frame
-> Capsule daemon stages under /jackin/run/clipboard/
-> Capsule pastes the container-readable path into the focused PTYThis fixes the structural cause: the host component that can read the host clipboard does so, then uses an explicit jackin protocol message to cross the container boundary. It also preserves the container boundary: agents receive a file path inside the container, not arbitrary host clipboard access.
A daemon remains relevant for future desktop/global bridges, but it was not required for the first correct implementation. Image paste is synchronous with an attached terminal and an explicit paste gesture. A short-lived host attach client can perform the needed host-side read without a long-running process.
Non-Goals
- Do not expose the host clipboard to agents as a general API.
- Do not let agents request clipboard reads without an operator paste gesture.
- Do not let agents open host browser windows merely by printing URLs, emitting OSC 8, or updating GitHub context.
- Do not let agents write files to the host merely by printing paths, emitting OSC links, or creating files in the container.
- Do not mount the host's general temp directory or Downloads directory into the container to "make paths work."
- Do not rely on OSC 52 clipboard reads as the primary implementation.
- Do not use Kitty graphics as the transfer path for image paste.
- Do not make Kitty/iTerm2 proprietary clipboard or file-transfer protocols required for the primary implementation.
- Do not add a persistent host daemon just for this feature.
- Do not implement provider-specific hacks for Claude Code, Codex, Amp, Kimi, or OpenCode as the foundation. Runtime-specific improvements can sit above the bridge after the generic bridge is correct.
Target Operator Experience
When an operator is attached to a jackin❯ Capsule session and presses the paste gesture while the host clipboard contains an image:
- jackin❯ reads the image from the host clipboard.
- jackin❯ stages it into the running role container.
- The focused agent prompt receives a readable container path, pasted in the same bracketed-paste-safe way as normal text paste.
- Claude Code, Codex, or any other image-aware CLI can attach/read that file using its normal local-file support.
- The operator sees a compact status cue such as
Image staged: /jackin/run/clipboard/clip-...png.
The behavior is the same whether the clipboard source is a raw screenshot, a copied browser image, a copied image file, or a drag/drop image file, subject to platform support and explicit permission policy.
The status cue reuses the same transient-feedback discipline as Selection copied: visible enough to confirm the host clipboard action happened, constrained to the multiplexer chrome/overlay surface, and never drawn over the pane prompt, status rows, footer hint, or bottom branch/run bar. Image staging feedback proves the staged container path, not a source host path or clipboard metadata.
Architecture
Host Attach Client
The host jackin binary owns the interactive terminal during attach instead of delegating the whole interactive session to docker exec -it ... /jackin/runtime/jackin-capsule.
This host client:
- Enters raw mode, enables bracketed paste, and owns terminal cleanup.
- Collects terminal identity and color information previously collected only by the in-container attach client.
- Connects to the Capsule attach protocol directly when the bind-mounted socket is usable from the host.
- Uses a stdio relay fallback through
docker exec -iwhen direct host connection to the Unix socket is impossible, especially Docker Desktop and OrbStack on macOS. - Forwards normal input bytes unchanged.
- Intercepts only explicit image-paste triggers and converts them into typed clipboard-image frames.
- Preserves copy-out behavior by forwarding focused-pane OSC 52 writes only when
JACKIN_OSC52=allow, and by continuing to send jackin-owned OSC 52 copy actions to the host terminal with the same visible feedback behavior operators already see. - Preserves host-terminal selection behavior. If the operator selects text directly in Ghostty's viewport and copies it, jackin❯ does not interfere; if the operator uses jackin-owned pane selection, jackin❯ continues to emit OSC 52 and render its own copy feedback.
- Provides the correct home for future host-only operator actions such as "open link under cursor", because a process inside the Linux role container cannot reliably open the operator's macOS or Linux desktop browser.
The first stage kept the existing in-container client for compatibility while adding a host client behind a feature flag (JACKIN_HOST_ATTACH=1). The end state removes the architectural dependency on the in-container client for host-terminal capabilities once the opt-in path is promoted to default.
Socket Connectivity
jackin❯ already bind-mounts a per-instance host directory at /jackin/run; the Capsule socket appears at ~/.jackin/sockets/<container>/jackin.sock. On same-kernel Docker hosts, host code can open that socket directly. On Docker Desktop for macOS, the host can see the inode through the bind mount but cannot connect to the Linux VM's Unix socket reliably. OrbStack is Docker-compatible but has its own macOS/VM filesystem and socket integration behavior, so it had to be tested as a separate local container backend rather than assumed equivalent to Docker Desktop.
The chosen portable attach strategy:
| Environment | Attach transport |
|---|---|
| Same-kernel Linux Docker | Host UnixStream directly to ~/.jackin/sockets/<container>/jackin.sock. |
| Docker Desktop macOS | Host process runs docker exec -i <container> /jackin/runtime/jackin-capsule attach-proxy; the proxy connects to /jackin/run/jackin.sock inside the container and relays framed bytes over stdio. |
| OrbStack macOS | Probe direct host connection to ~/.jackin/sockets/<container>/jackin.sock; if connect fails, use the same docker exec -i ... attach-proxy relay. The implementation records which path was selected in debug logs. |
| Future remote Docker / Kubernetes | Host-side attach transport becomes another adapter, but the clipboard-image frame and staging contract remain the same. |
This keeps the image bridge independent of Docker implementation details. The protocol says "client sends ClipboardImage"; the transport decides how bytes reach the daemon.
The implementation reuses the existing snapshot/control precedent: try the host-visible socket path first when the Docker backend makes that meaningful, then fall back through an in-container command when the host can see the socket inode but cannot connect through the VM boundary.
Protocol
ClipboardImageFormat is an explicit tag, not a free-form extension string. The first implementation supports Png, Jpeg, Gif, Webp, and Tiff; Capsule validates the claimed format against image magic bytes before staging.
Normal paste stays ClientFrame::Input. A host image paste is not encoded as base64 text inside a terminal paste sequence; it is a typed frame so Capsule can validate, stage, log, and route it deliberately.
The normal MAX_FRAME_PAYLOAD is 4 MiB. Small clipboard-image frames deliberately use a separate 16 MiB frame cap and expose MAX_CLIPBOARD_IMAGE_BYTES = 16 MiB - 1 for the one-byte format discriminator. Larger image paste uses chunked client-to-server frames (ClipboardImageStart / ClipboardImageChunk / ClipboardImageEnd), each chunk capped at 1 MiB, with a 64 MiB first-implementation transfer cap. Capsule assembles by transfer id, enforces contiguous offsets and total byte count, verifies SHA-256, and cancels the active transfer id on a malformed chunk. File export uses the equivalent server-to-host chunked shape (FileExportStart / FileExportChunk / FileExportEnd), each chunk capped at 1 MiB with a 64 MiB export cap, writing to a temporary host file under ~/Downloads/jackin/<instance>/, verifying byte count and SHA-256, then renaming to a collision-resistant final path.
Broader file export and very large binary transfers still need more transfer work: a negotiated side stream that still routes through Capsule policy and telemetry, and explicit cancellation frames for interrupted image/file transfers if live validation proves they are useful.
Host Clipboard Readers
Each backend is isolated behind one trait (HostClipboardImageReader) so platform-specific reads stay swappable:
| Platform | Primary backend | Fallback |
|---|---|---|
| macOS | Native pasteboard access through a small Rust/ObjC or Swift helper, or a maintained Rust clipboard crate if it can read images reliably. | osascript coercing the pasteboard to PNG/TIFF, used as a compatibility fallback and test oracle. |
| Wayland Linux | wl-paste --type image/png and sibling image MIME types. | None initially; return "no image" if unavailable. |
| X11 Linux | xclip -selection clipboard -t image/png -o and sibling image MIME types. | xsel if it can read typed image selections reliably. |
| Windows | Out of scope until native Windows host support exists. | None. |
The reader tries image MIME types in a deterministic order and validates the returned bytes against the claimed format. It never trusts a clipboard command just because it exited successfully.
Supported formats: PNG (\x89PNG\r\n\x1a\n signature), JPEG (0xFF 0xD8 0xFF prefix), GIF (GIF87a/GIF89a), WebP (RIFF....WEBP), TIFF (MM\0*/II*\0 prefix). SVG is not accepted as an image clipboard payload in V1: it is text with active-content and parser concerns. If needed later, it should be a separate explicit file-staging path with stricter sanitization.
Container Staging
Capsule stages received images under /jackin/run/clipboard/. The staged files are jackin-owned runtime state and therefore belong under /jackin/, specifically the ephemeral runtime mount, rather than a new top-level container path or host path family.
/jackin/run/clipboard/ is runtime staging, not durable artifact storage. It is acceptable for clipboard images that need to survive long enough for the agent to read them after prompt submission. It is not the destination for exported host files, diagnostics archives, or user-kept artifacts.
Staging rules: create the directory with 0700, create files with 0600 and create_new, generate collision-resistant names that do not include host paths or clipboard text, use the extension only after sanitization, re-validate magic bytes in Capsule before writing even if the host reader already validated, return the staged in-container path to the paste router, and delete staged files on clean daemon exit and SIGINT/SIGTERM shutdown when possible (always on instance socket-dir cleanup; ordinary client reattach does not remove staged files because a running agent may still need to read a just-pasted path).
Paste Routing
After staging, Capsule pastes the path into the focused PTY using the same bracketed-paste semantics as normal text paste: bracketed-paste wrapper if the focused pane has bracketed paste enabled, otherwise ordinary bytes. Enter is not appended automatically in V1, so the operator reviews the prompt before submission. If no pane is focused, or the focused pane is not writable, Capsule reports the staged path and a not pasted warning instead of dropping the image silently.
Host Link Opening
The design goal for host link opening:
- Preserve Capsule's existing OSC 8 passthrough so terminals with native hyperlink support keep working.
- Track safe hyperlink regions in the focused pane and jackin-owned chrome as queryable frame metadata.
- Add an attach request such as
OpenLinkAt { row, col }orLinkAt { row, col }so the host client can ask Capsule what link lives under a mouse event. - Add a direct
OpenUrl { url, source }request for jackin-owned chrome and dialogs that already carry an exact URL, starting with GitHub context's PR and CI/check URLs. - Fall back to plain-text URL detection in the visible row when no OSC 8 region exists, reusing the selection URL-token rules instead of adding a second URL parser.
- Support both primary-screen transcript output and alternate-screen rich TUI output by querying Capsule's composed grid, not by scraping terminal scrollback from the host terminal emulator.
- Render modifier-hover disclosure as a compositor overlay derived from the current frame's link metadata, never writing highlight escape sequences into the agent PTY.
- Restrict openable schemes to
http,https, andmailtounless a later operator setting explicitly adds more. - Execute the browser-open command only in the host process.
- Never open a URL just because an agent emitted OSC 8 or printed text.
This shares the same transport foundation as image paste: the host client owns the operator gesture and host-side action; Capsule owns terminal state, pane focus, and URL safety classification.
Container File Export
The inverse of host-to-container image/file staging is container-to-host export. Operators often need to inspect or keep artifacts created by agents: screenshots, generated images, logs, patch files, archives, reports, or build outputs. Before this feature they had to manually use docker cp, bind mounts, or copy text out of the pane.
The implemented data flow:
Operator selects / points at a container path
-> Capsule validates path and file metadata inside the container
-> Capsule streams file bytes over the attach protocol
-> host attach client writes to an explicit host destination
-> operator sees Exported: ~/Downloads/jackin/<instance>/...Destination policy: default to a jackin-owned host download root (~/Downloads/jackin/<workspace-or-instance>/), preserve the basename but sanitize it and append a collision-resistant suffix when needed, never write to Desktop, Downloads root, a user repository, or an arbitrary host path without an explicit file-picker/CLI destination, and surface the destination before or immediately after export.
Validation and transfer rules: resolve the requested path inside the container before transfer and reject symlink escapes outside the workspace and /jackin/run/; allow workspace files and jackin-owned runtime files by default, with other container paths requiring later confirmation or a policy setting; start with regular files only (directory archive export is a separate later action); apply size caps and stream in chunks (1 MiB chunks, 64 MiB V1 cap); compute a digest during transfer and report bytes + digest in debug logs; never log file contents (compact logs use path category, basename, byte count, destination category, coarse rejection reason, and result, not full container paths or full host destinations — host notices may show the exported destination because they are the operator-facing confirmation for the explicit export action).
This uses typed attach frames rather than shelling out to docker cp as the product architecture, because docker cp bypasses Capsule's path policy, focus/session ownership, telemetry, and future remote-transport abstraction. A docker cp helper can remain a manual fallback outside the multiplexer.
Observability
Compact logs: clipboard-image: paste trigger source=clipboard, clipboard-image: staged extension=png bytes=123456 path=/jackin/run/clipboard/..., clipboard-image: rejected reason=oversize stage=transfer-start, clipboard-image: rejected reason=signature-mismatch stage=payload, clipboard-image: no-image source=clipboard text-paste=forwarded, host-link-open: opened scheme=https source=osc8, host-link-open: opened scheme=https source=github-context-pr, host-link-open: opened scheme=https source=github-context-ci, host-link-open: rejected reason=unsafe-scheme scheme=file, host-file-export: requested category=workspace basename=report.md, host-file-export: exported bytes=123456 dest=downloads/jackin/..., host-file-export: rejected reason=oversize bytes=... max=..., host-file-export: rejected reason=path-policy path_category=container-root.
Debug logs: which host backend was attempted; which MIME type was tried; why each backend returned no image; clipboard command exit status or platform error, with no image bytes and no base64; attach transport path (direct socket vs stdio proxy); for host link opening, the mouse/modifier event, link source (osc8, chrome metadata, or plain text), policy decision, and opener command exit status, with query strings and fragments redacted; for host file export, path resolution category, transfer id, byte count, digest, destination category, and failure cause, without file contents.
Never log image contents, base64 payloads, source host paths unless the user explicitly used a file-staging command, clipboard-derived metadata that may contain private content, or full URL query strings from host link opening.
Host-side Effects
This roadmap authorizes five classes of host-side effects, all gated by an explicit operator gesture:
| Effect | Trigger | Host mutation / action | Required guardrails |
|---|---|---|---|
| Clipboard image paste | Operator invokes paste while the attach client is focused and the host clipboard contains an image. | Read host clipboard; copy validated bytes into per-instance jackin-owned runtime state visible as /jackin/run/clipboard/... inside the container. | No general clipboard API for agents; no host paths pasted; no image bytes in logs; size and format caps. |
| Clipboard text-path image staging | Operator pastes a single absolute host image path (default-on auto-stage), or invokes the command-palette Stage image from clipboard path action; the referenced host file must be a real image. | Read the referenced image file from the host (auto-stage reads only the pasted bytes; the palette action reads host clipboard text); copy validated bytes into per-instance jackin-owned runtime state visible as /jackin/run/clipboard/... inside the container. | Absolute image files only, validated by magic bytes; host path is never pasted; ordinary non-image text stays text; auto-stage rejects prose, relative paths, missing files, and mixed pastes, and is opt-out via JACKIN_PASTE_IMAGE_PATHS=0; same caps as direct image paste. |
| Open link in browser | Operator invokes an open-link gesture, such as modifier-click or command palette action, on a safe URL. | Launch the host browser/opener for that URL. | User gesture required; scheme allowlist; operator opt-out; no automatic opens from agent output. |
| Open GitHub context targets | Operator invokes an explicit GitHub context action such as "open PR" or "open CI". | Launch the host browser/opener for the known PR or CI/checks URL. | User gesture required; target is derived from trusted jackin❯ GitHub context state; failing CI should prefer the failing run/check URL when known. |
| Export container file | Operator invokes an explicit export action on a resolved container file path. | Write a copy under a surfaced host destination, defaulting to a jackin-owned Downloads subdirectory. | User gesture required; path policy; regular-file-only V1; size cap; no arbitrary host destination without explicit file picker/CLI destination. |
None of these effects writes host configuration, shell files, git state, browser profiles, or user repositories. If a later implementation needs persistent host preferences, they must live under jackin-owned host state and be surfaced before the behavior ships.
Security Model
This feature must preserve the rule that jackin❯ does not silently mutate or expose the host.
The allowed host-side effects are deliberately narrow: read the host clipboard image data on an operator paste gesture, read host clipboard text plus one absolute image path on the explicit image-stage action, open a safe URL on an operator open gesture, open known GitHub context targets on an operator dialog action, and write an exported container file to a surfaced host destination on an operator export action. The host clipboard, browser, and filesystem are not exposed to the agent as APIs. Image bytes are copied into per-instance jackin-owned runtime state, URLs are opened only by the host attach client, and exported files are written only by the host attach client after Capsule validates and streams the selected container file.
Threats and mitigations:
| Threat | Mitigation |
|---|---|
| Malicious agent tries to read clipboard repeatedly. | Agents cannot invoke the host reader. Only the host attach client reads the clipboard on operator paste. |
| Agent prints a host image path and expects jackin❯ to auto-import it. | Agent-printed output is never auto-staged: only an operator paste (a bracketed paste in the input stream) or an explicit palette action can stage, and only when the content is a single absolute path to a real readable image file. Plain prose and non-image text stay ordinary text; the paste auto-stage is opt-out via JACKIN_PASTE_IMAGE_PATHS=0. |
| Malicious pane tricks the operator into pasting secrets. | This is the same class as ordinary paste. Add visible staging feedback and never auto-submit. |
| Clipboard command returns text for an image MIME request. | Validate magic bytes before staging. |
| Oversized clipboard image exhausts memory or socket bandwidth. | Apply host read cap, protocol image cap, and Capsule staging cap. |
| Staged file leaks after session. | Keep files under per-instance /jackin/run/clipboard/; remove on detach and instance cleanup. |
| Host path leaks into the prompt. | Generate container-local paths; do not paste host paths. |
| Any same-UID process injects image frames into the socket. | Preserve existing 0700 socket directory and 0600 socket file permissions; typed frame validation prevents arbitrary file writes outside staging. |
| Image parser vulnerability. | Do not decode pixels in jackin❯ V1. Validate only small magic-byte/header checks and pass bytes through as files. |
| Agent spams OSC 8 to open browser windows. | Host link opening is gesture-gated; OSC 8 alone only marks a region and never opens anything. |
| Link opens a dangerous local or active-content URI. | Restrict schemes to http, https, and mailto; keep file, javascript, data, and custom app schemes blocked unless explicitly configured later. |
Agent prints /etc/shadow and tricks export. | Export requires explicit operator action and path policy; sensitive container paths outside workspace and /jackin/ require confirmation or are denied by default. |
| Export follows a symlink to an unexpected location. | Resolve and validate paths inside Capsule before streaming; reject symlink escapes when exporting from a restricted root. |
| Export overwrites host files. | Host client uses create-new or collision-resistant names by default; arbitrary overwrite requires explicit file-picker confirmation. |
| Large export exhausts memory or disk. | Stream chunks, enforce caps, surface byte counts, and fail cleanly before writing beyond policy. |
Correctness Invariants
- Every host clipboard image accepted by jackin❯ becomes a file readable from inside the focused agent container.
- No host-only path is pasted to an agent unless the same path is intentionally mounted and readable inside the container.
- Every staged path lives under
/jackin/run/clipboard/. - An image paste never becomes raw terminal base64.
- An image paste never auto-submits unless a later explicit operator setting enables that behavior.
- A failed image paste is visible to the operator.
- Normal text paste behavior remains byte-for-byte compatible unless the host clipboard reader positively detects an image on an explicit image-paste trigger.
- Plain text paths remain ordinary terminal text unless the operator invokes the explicit image-path staging action.
- Same-kernel Docker, Docker Desktop, and OrbStack use the same attach protocol; only transport differs.
- Existing copy-out behavior remains intact: pane OSC 52 still reaches the host terminal when explicitly allowed, while jackin-owned selection copy, double-click word copy, and copyable dialog rows still render visible feedback.
- Image-stage feedback follows the same overlay constraints as
Selection copied: it must not cover the prompt cursor, pane input line, status bar, footer hint, or branch/run bar. - A host browser open happens only after an explicit operator gesture and only from the host attach client or a future daemon/Desktop surface with the same approval semantics.
- Links emitted by an agent are inert until the operator clicks or invokes an open command.
- Plain-text URL detection uses one shared URL-token rule with double-click selection; there must not be separate subtly different URL grammars for copying and opening.
- Rich TUI mouse fidelity remains intact: unmodified mouse events and mouse events without a safe link target continue to the focused pane exactly as they do today.
- Link-hover highlighting is presentational only; it never mutates the PTY, never changes pane scrollback, and never rewrites the agent's rendered cells.
- jackin-owned dialogs that already know URLs expose explicit open actions instead of forcing the operator through text selection, terminal-native hyperlinks, or modifier-click detection.
- GitHub context's CI open action resolves to the most useful available target: failing job/check URL first, then workflow run URL, then PR checks tab or repository Actions page only as a fallback.
- Container file export is always a copy from container to a surfaced host path; agents never receive a writable host directory as a side effect of export.
- Exported host files use create-new/collision-resistant writes by default and never overwrite operator files silently.
- Export path resolution happens inside Capsule against the container filesystem before any host write begins.
Alternatives Considered
Keep the current docker-exec attach client and patch Capsule input parsing
This is insufficient because the in-container attach client cannot read the host clipboard. It can only parse bytes that arrive through Docker's PTY. If Ghostty or the OS never sends image bytes to stdin, no parser can recover them.
Stage image files on the host and paste host paths
This works only when the host path is mounted inside the container at the same path. That is false for general host temp paths, Finder files, screenshots, and most clipboard sources. Pasting host paths is the exact broken state the feature is meant to eliminate.
Bind-mount a host clipboard staging directory and paste mapped paths
This is a viable lower-level mechanism, but it should be hidden behind the Capsule staging contract rather than exposed as the product architecture. The simplest implementation may write bytes to the bind-mounted /jackin/run/clipboard/ directory from the host so the container sees them immediately. The invariant still needs to be "agent sees /jackin/run/clipboard/...", not "agent sees some host temp path."
Use OSC 52 clipboard query
This is not portable enough, is text-oriented, often requires user consent, and does not solve file staging inside the container. It can remain a possible text clipboard enhancement but not the image bridge foundation.
Use Kitty graphics
Kitty graphics is for displaying images in the terminal. The feature needs to import host clipboard images into the agent's filesystem/context. Display and input are different directions.
Add a persistent host daemon first
A daemon can solve this and will be useful for broader host bridge features, but it is not required for explicit paste while a host attach client is active. A daemon-first design also risks conflating "operator pasted into this focused terminal" with "agent can ask the host to read clipboard later." Those are different permissions and should remain separate.
Agent-specific direct attachments
Codex and Claude Code may have internal prompt attachment APIs that would let jackin❯ inject an image more semantically than pasting a path. That can be an enhancement after the generic bridge exists. The correct base layer is agent-independent: stage a file where the runtime can read it and paste a path.
Rely only on terminal-native hyperlink clicking
Forwarding OSC 8 is necessary and already part of Capsule's terminal-fidelity model, but it is not sufficient. Some terminals do not expose the desired modifier-click behavior, and plain text URLs are not OSC 8 links. A host-owned open-link action makes the experience consistent while preserving terminal-native hyperlinks where they work.
Use docker cp as the file export product surface
docker cp is a useful manual fallback, but it is not the right user-facing Capsule feature. It requires the operator to know the container name and exact path, bypasses the focused session and path policy, does not integrate with hover/dialog actions, and will not compose with future remote Docker or Kubernetes transports. Capsule export should be typed, policy-checked, logged, and available from the same multiplexer UI that showed the file path.
Bind-mount a host export directory into every container
This makes export mechanically easy but violates the directionality that keeps the host safe. A writable host mount gives agents an ambient host write surface. The correct export direction is pull-based: the operator requests a copy, Capsule streams bytes out, and the host client writes to a surfaced destination.
Expected Code Touchpoints
- Host launch and attach:
crates/jackin/src/app/load_cmd.rs,crates/jackin-runtime/src/runtime/attach.rs,crates/jackin-runtime/src/runtime/launch.rs, andcrates/jackin-runtime/src/runtime/snapshot.rs. - Capsule PID split, socket, and attach protocol:
crates/jackin-capsule/src/main.rs,crates/jackin-capsule/src/socket.rs, andcrates/jackin-protocol/src/attach.rs. - Current terminal client behavior to preserve:
crates/jackin-capsule/src/tui/run.rs,crates/jackin-capsule/src/tui/terminal.rs,crates/jackin-capsule/src/tui/host_colors.rs, andcrates/jackin-capsule/src/tui/input.rs. - Pane input/output and OSC policy:
crates/jackin-capsule/src/session.rs,crates/jackin-capsule/src/daemon.rs, andcrates/jackin-capsule/src/daemon/mouse_input.rs. - GitHub context data and dialog UI:
crates/jackin-capsule/src/pr_context.rs,crates/jackin-capsule/src/pull_request.rs, andcrates/jackin-capsule/src/tui/components/dialog.rs. - Existing host-private write posture reused conceptually for create-new, permissioned, symlink-safe writes:
crates/jackin-instance/src/auth.rs. Credential-specific helpers were not reused directly as a generic export API without extracting the policy boundary. - Host clipboard readers and paste-path auto-stage:
crates/jackin-host/src/host_clipboard.rsandcrates/jackin-runtime/src/runtime/host_attach.rs. - Command palette actions:
crates/jackin-capsule/src/tui/components/palette.rs.
Phasing
The phases below were the recommended sequence for landing this without mixing future ideas into V1.
Phase 0 — validation and protocol design
Phase 0 Ghostty paste/click/motion measurements, Cmd/Ctrl/Alt modifier visibility, OrbStack transport probing, Amp xclip behavior, and GitHub CI URL query shape are captured in the preflight page.
Phase 1 — host attach foundation
Host-owned attach code landed behind JACKIN_HOST_ATTACH=1: the host runtime owns raw mode, stdin/stdout, resize, focus/mouse bytes, cleanup, shared protocol framing, direct/proxy transport selection, and the shared OSC 10/11 default-color query, with direct-socket attach for same-kernel Docker hosts and an attach-proxy stdio relay subcommand for Docker Desktop/OrbStack fallback.
Phase 2 — V1 affordances
ClientFrame::ClipboardImage landed as a bounded single-frame message with explicit format tags and a MAX_CLIPBOARD_IMAGE_BYTES cap, plus chunked ClipboardImageStart/ClipboardImageChunk/ClipboardImageEnd frames for larger payloads. Capsule staging under /jackin/run/clipboard/, macOS PNG/TIFF/Finder-file-url clipboard reading, the explicit Stage image from clipboard path and Stage image without pasting command-palette actions, the GitHub context dialog's open PR/open CI actions, JACKIN_OPEN_LINKS opt-out, and minimal container file export all landed in this phase.
Phase 3 — platform completeness
Planned follow-ups: a native macOS pasteboard reader beyond osascript, both Wayland and X11 Linux backends (rather than a single one), copied-file-URL support beyond Finder, and live validation of Linux clipboard failure messaging on real hosts.
Phase 4 — runtime-aware and interaction polish
Planned follow-ups: runtime-aware paste text shape for CLIs with documented image-attachment syntax, richer transcript-mode and rich-TUI link hover styling, first-class export/reveal/open actions on more artifact surfaces as they gain typed metadata, and a shared transient-feedback queue so multiple status cues (Selection copied, Image staged, export completion) do not hide each other.
Phase 5 — future-only daemon, desktop, and remote work
Deferred to a future daemon/Desktop Agent Hub: remote/background clipboard reader and browser-open actions with the same operator-gesture gating, save dialogs and destination preferences, drag/drop path support, generic arbitrary file paste/upload, directory/archive export, and cross-host relay for remote/Kubernetes agents following the same "read host clipboard, transfer to target runtime, paste target path" invariant.
Open Design Questions
Paste trigger
What should count as an explicit image-paste trigger? Options considered: Ctrl+V detected in raw input (matching herdr's first implementation), empty bracketed paste while the host clipboard contains an image, a jackin-owned command palette action, or terminal-specific paste events if a host terminal library exposes them. The chosen answer: implement the command palette action and raw Ctrl+V first, add terminal-specific mappings only when measured. Cmd+V on macOS is usually handled by the terminal emulator before terminal bytes exist, so jackin❯ had to verify what Ghostty actually sends.
Host clipboard crate
Should jackin❯ use a cross-platform Rust crate or platform-specific helpers? The answer should be decided by capability, not convenience. A crate is correct if it can read image payloads, file promises, and common MIME types without pulling in an unmaintained dependency. If no crate satisfies that, use platform helpers behind a small internal trait.
Where to stage bytes
Two mechanically possible staging paths: the host writes into the bind-mounted host directory backing /jackin/run/clipboard/ and sends a small frame telling Capsule the file is ready, or the host sends image bytes in a frame and Capsule writes the file. The frame approach is cleaner and centralizes validation in Capsule; the host-write approach avoids sending large payloads over the socket and may work better on Docker Desktop or OrbStack if stdio relay throughput is a real limit. Either path must produce a Capsule-validated file under /jackin/run/clipboard/, not a host path.
Multi-client attach
Capsule currently has one active attach client. If a future daemon/Desktop client also observes the same session, image paste should route from the foreground/writable client only. Read-only observers must not be able to inject clipboard frames.
Cleanup lifetime
Should staged images last until detach, until the tab closes, until container cleanup, or for a fixed TTL? Chosen answer: keep for the container lifetime, under /jackin/run/clipboard/, and rely on instance cleanup. Removing immediately after paste is wrong because agents may read the file after the prompt is submitted, not at paste time.
Non-image file paste
Image paste naturally raises "what about PDFs, logs, and arbitrary files?" V1 stays image-specific because agent CLIs have direct multimodal image support and image signatures are easy to validate without decoding. General file paste is a separate "host file staging" feature with a broader security and policy surface.
Link opener policy
Should host link opening be enabled by default or require a setting? Chosen answer: enable only for explicit operator gestures and safe schemes, and still provide an environment/config opt-out (JACKIN_OPEN_LINKS). Opening a URL is an observable host-side action, but a modifier-click or command palette action is already an operator request. The opt-out exists for untrusted roles or terminals where users prefer native handling only.
GitHub context CI target
What URL should the GitHub context dialog open when CI is failing? Chosen answer: store enough check metadata to open the most specific failing surface. If there is exactly one failed job, open that job. If several jobs failed in one workflow run, open the run summary. If failures span runs, open the PR checks tab with the failed contexts visible. Falling back to the repository Actions page is valid only when jackin❯ does not have check/run URLs.
File export destination
Where should exported files land? Chosen answer: default to ~/Downloads/jackin/<workspace-or-instance>/ with create-new writes and a visible destination cue. An explicit --to <path> / file-picker destination for one-off exports remains a possible later addition. Do not default to Desktop, the current repository, or the host path that "looks equivalent" to a container workspace path.
File export path policy
Which container paths can be exported without confirmation? Chosen answer: workspace paths, /jackin/run/clipboard/, /jackin/run/uploads/, and jackin-owned diagnostics/artifact paths. Everything else either requires confirmation or is denied initially. This keeps the feature useful for generated artifacts while preventing a broad "copy anything from the container" footgun.
Directory export
Should directories be supported? Chosen answer: not in V1. Start with regular files only. Directory export should be explicit and archived inside Capsule as tar.zst with path normalization, symlink policy, size limits, and a clear destination filename.
Related Work
- Host Affordance Bridge — current status and remaining work this design record supports.
- Host Affordance Bridge Preflight — Phase 0 measurements that grounded this design.
- herdr — reference architecture for host clipboard image detection, typed image payloads, staging, and path paste.
- tmux clipboard wiki — reference for OSC 52 text clipboard limits and security concerns.
- Kitty graphics protocol — relevant to image display, not image paste transfer.
- Ghostty discussion #10478 — concrete evidence that raw macOS screenshot clipboard paste and file-backed image paste behave differently in terminals.
- Ghostty discussion #10517 — SSH image paste request with the same "upload locally held image, paste remote path" product invariant.
- WezTerm
OpenLinkAtMouseCursorandhyperlink_rules— reference for explicit open-link actions and implicit URL detection. - xterm.js Link Handling — reference for modifier-gated activation and URL hover disclosure.
- Rio Hyperlinks — reference for configurable hyperlink modifier keys and platform defaults.
- iTerm2 shell integration — reference for host-side upload/download actions tied to remote terminal context.
- WezTerm shell integration — reference for OSC 7 / OSC 133 / OSC 1337 prompt and pane metadata.
- Kitty clipboard kitten — reference for terminal-mediated arbitrary clipboard MIME transfer with permission prompts.
- Kitty remote files — reference for opening, editing, and downloading remote files from visible terminal paths.
- Kitty transfer kitten — reference for bidirectional TTY file transfer.
- iTerm2 utilities — reference for a remote-file download helper that lands files in Downloads.