Host-Guest Socket Protocol
Wire Format
Length-prefixed JSON over a Unix stream socket:
Socket Location
| Socket | Path | Created by |
|---|---|---|
| Host socket | $XDG_RUNTIME_DIR/podbox/<name>.sock | .socket Quadlet unit |
| Local guest socket | /run/podbox/guest-<name>.sock | podbox-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:
1{2 "type": "hello",3 "version": "0.1.0",4 "container": "myenv",5 "capabilities": ["notify", "xdg_open", "clipboard", "host_exec"]6}Host responds:
1{2 "type": "hello_ack",3 "accepted": ["notify", "xdg_open"],4 "rejected": ["clipboard", "host_exec"],5 "idle_timeout_secs": 06}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
| Type | Fields |
|---|---|
hello | protocol_version, guest_version, container, capabilities |
notify | summary, body, urgency, actions (optional), app_name (optional) |
xdg_open | uri |
clipboard_set | text (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_exec | cmd, args |
host_exec_v2 | cmd, 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_signal | sig (optional signal number to relay to a running fd-passing host-exec child) |
register_session | — (pidfd via SCM_RIGHTS) |
busy | — |
idle_timeout | — |
Host → Guest
| Type | Fields |
|---|---|
hello_ack | accepted, rejected, idle_timeout_secs |
clipboard_data | text |
host_exec_stdout | data |
host_exec_stderr | data |
host_exec_output | stream (stdout/stderr), data (raw bytes) — chunked streaming output, only when the guest negotiated host_exec_stream |
host_exec_done | exit_code, signal (optional; the killing signal when the child died of one — exit_code is then 128 + signal) |
notify_action_result | notification_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.
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.
| Capability | Interceptor | Description |
|---|---|---|
notify | notify-send | Desktop notification forwarding |
xdg_open | xdg-open | URI opening via host |
clipboard | podbox-clipboard | Clipboard sharing |
host_exec | host-exec | Execute 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.