Skip to main content
On this page

Host-Guest Socket Protocol

Wire Format

Length-prefixed JSON over a Unix stream socket:

Wire format Each message is a 4-byte big-endian length followed by that many bytes of UTF-8 JSON. Example: 00 00 00 0F followed by the 15 bytes {"type":"ping"}. bytes 0–3 bytes 4 … N+3 length = N u32, big-endian payload N bytes of UTF-8 JSON, one message example 00 00 00 0F = 15 {"type":"ping"} 15 bytes Frames over 16 MiB are rejected. Both directions use the same framing.
Message wire format

Socket Location

SocketPathCreated by
Host socket$XDG_RUNTIME_DIR/podbox/<name>.sock.socket Quadlet unit
Local guest socket/run/podbox/guest-<name>.sockpodbox-guest --daemon

The host socket is created by systemd before the container starts and persists across restarts. The guest socket is used by interceptor processes to communicate with the local daemon.

Handshake

Guest sends:

json
1{
2 "type": "hello",
3 "version": "0.1.0",
4 "container": "myenv",
5 "capabilities": ["notify", "xdg_open", "clipboard", "host_exec"]
6}

Host responds:

json
1{
2 "type": "hello_ack",
3 "accepted": ["notify", "xdg_open"],
4 "rejected": ["clipboard", "host_exec"],
5 "idle_timeout_secs": 0
6}

The handshake decides which capabilities the guest may use (0 timeout = disabled). The guest only installs interceptor symlinks for accepted ones.

Message Types

Guest → Host

TypeFields
helloprotocol_version, guest_version, container, capabilities
notifysummary, body, urgency, actions (optional), app_name (optional)
xdg_openuri
clipboard_settext (at most 8 MiB; larger content gets an error reply and leaves the session open)
clipboard_get— (the host answers error when the selection exceeds 8 MiB)
host_execcmd, args
host_exec_v2cmd, args, env (optional key/value pairs; only TERM, LANG, COLORTERM pass), cwd (optional guest working directory; interpreted per the configured cwd handling, defaulting to the host $HOME), tty (optional; see TTY sessions below) — the child's stdin/stdout/stderr arrive as three descriptors via SCM_RIGHTS on the same connection
host_exec_signalsig (optional signal number to relay to a running fd-passing host-exec child)
register_session— (pidfd via SCM_RIGHTS)
busy—
idle_timeout—

Host → Guest

TypeFields
hello_ackaccepted, rejected, idle_timeout_secs
clipboard_datatext
host_exec_stdoutdata
host_exec_stderrdata
host_exec_outputstream (stdout/stderr), data (raw bytes) — chunked streaming output, only when the guest negotiated host_exec_stream
host_exec_doneexit_code, signal (optional; the killing signal when the child died of one — exit_code is then 128 + signal)
notify_action_resultnotification_id, action_key
ping—
check_idle—
shutdown—

Notify actions

actions is an optional array of {key, label}. The host replies with notify_action_result carrying the notification_id and the chosen action_key.

json
1{
2 "type": "notify",
3 "summary": "Build complete",
4 "body": "Exit code: 0",
5 "actions": [
6 { "key": "open", "label": "Open project" },
7 { "key": "dismiss", "label": "Dismiss" }
8 ]
9}

Older guests omit actions/app_name — both default empty server-side.

Capabilities

One interceptor symlink per capability. Rejected ones are skipped silently — no symlink, no retries.

CapabilityInterceptorDescription
notifynotify-sendDesktop notification forwarding
xdg_openxdg-openURI opening via host
clipboardpodbox-clipboardClipboard sharing
host_exechost-execExecute commands on host
host_exec_stream— (requested by host-exec alongside host_exec)Chunked raw-byte output streaming; without it the host buffers one frame per pipe
host_exec_fds— (requested by host-exec alongside host_exec)Attached-stdio sessions: host_exec_v2 requests carry the child's three stdio descriptors via SCM_RIGHTS, so bytes flow through the pipes themselves with no protocol copying; host_exec_signal relays signals to the child

A guest that predates host_exec_stream never requests it and keeps receiving buffered host_exec_stdout/host_exec_stderr frames, and it parses the newer host_exec_done fine (the extra signal field is ignored). No protocol version bump was needed.

host_exec_fds follows the same rule. A guest without it keeps using the plain host_exec frames exactly as before; a host that predates the capability never grants it, so an acknowledging guest falls back to the plain path. The child spawned for an fd-passing session sits in its own process group, so Ctrl-C typed in the guest's terminal reaches it as a relayed SIGINT rather than killing anything else; the exit is still reported through the regular host_exec_done frame.

TTY sessions

A fd-passing host_exec_v2 request with tty = true carries the slave end of the guest's own pty three times (one descriptor per stdio slot). The guest allocates the pty when its stdout is a terminal, copies its terminal's termios and window size onto the slave, and keeps the master open until the session's exit report arrives — dropping it hangs the terminal up.

While the session runs, the guest shim relays bytes in both directions: its stdin into the master, and the master onto its stdout. It also puts its own terminal into raw mode for the duration, because the child's pty already carries the original termios and therefore owns line editing and echo; it restores the caller's termios when the child exits. The guest closes its own slave copies once the descriptors have been sent, so the relay reaches end-of-file when the child exits rather than waiting for the shim itself to return.

The host runs the child as the new session's leader: setsid gives it a process group of its own (the same group host_exec_signal targets), and TIOCSCTTY on the inherited stdin makes the pty its controlling terminal, so the kernel delivers Ctrl-C, job control and window-size changes on the child directly, the way an interactive shell expects. An error in that setup aborts the spawn, and the guest sees the same typed failure a missing command does.

Everything else on the session is unchanged: the cwd policy, the allowlist and argument filter apply the same way, output flows through the descriptors rather than the protocol, and the exit arrives as one host_exec_done. The per-entry timeout (below) never applies to a tty session: an interactive child outliving its request is correct behaviour, not a leak. A window-size change in the guest terminal is copied onto the pty and its SIGWINCH relayed, so full-screen programs track resizes in either direction.