Venom
RFC-0017venom-spec

Standalone venomx entry point

Field Value
Status Accepted
Accepted 2026-11-07, explicitly approved by the maintainer
Authors commixon (commixon), with Claude
Created 2026-11-07
Linear COM-292; implementation COM-295 (venomx), COM-301 (venom-os)
Supersedes None; amends RFC-0009 sections 2 and “Alternatives”, and RFC-0007 section 5 for opted-in panes
Superseded by None

History

  • 2026-11-08 (venom-spec#92, normative, compatible addition; explicitly accepted by the maintainer on 2026-11-09): §4 rule 2 refuses to start a daemon below a manager, as RFC-0009 section 2 now does.

  • 2026-11-08 (COM-305, normative; accepted through the COM-305 PR review): §3 says how new starts the default shell as a login shell, using the program parameter RFC-0013 now defines. The behaviour §3 asked for does not change.

Summary

venomx should be as simple as tmux when used alone: type venomx, get a shell, detach and reattach, and the server goes away when the last shell exits. Under venom-os the same binary must instead run as an always-on daemon. This RFC makes whoever starts the daemon decide its lifetime: a human’s interactive venomx starts an on-demand daemon, a supervisor’s venomx serve runs a persistent one, and agents and scripts never start one. It adds the interactive local commands venomx / venomx new and a selector-less venomx attach, which compose existing verbs on the client with no new daemon mutation path, and a per-pane on_exit policy so that exiting the last shell ends the session. venomx implements it; venom-os adopts the supervised serve.

Motivation

Today a human who wants one shell in venomx types, from the README:

venomx serve
VENOMX_TOKEN=@owner venomx session create --name work --json
VENOMX_TOKEN=@owner venomx window create --session s0 --argv '["/bin/sh"]' --json
venomx attach --session s0

and typing exit in that shell leaves an exited pane, a window and a session behind (RFC-0007 section 5), so nothing ever ends on its own. tmux needs one word. venomx is meant to be “a tmux replacement that agents can drive” (ecosystem doc, section 3); without this RFC it is only the second half of that sentence.

RFC-0009 already supplies the hard part: an on-demand lifetime, socket arbitration, readiness and last-session shutdown, all opt-in. What it deliberately left out is a human entry point. It rejected “attach implicitly creates a session/shell” because that would mix presentation with mutations. This RFC keeps that concern and answers it differently: the entry point is a client-side sequence of ordinary verbs, each authorized by the daemon in the normal way.

Waiting on this RFC: daily-driving venomx (P1 exit criteria), venom-os’s supervisor design (P5) and venom’s ui.chrome = .auto (RFC-0016 section 10).

Design

1. Examples

First run, no daemon:

$ venomx

The terminal shows a shell in session s0, window w0, with the status line of RFC-0016. Behind the scenes the CLI started venomx serve --lifetime on-demand --origin interactive, then ran session.create, window.create with the user’s shell, and attached.

(inside venomx) $ exit
[venomx: session s0 ended; server stopped]
$ venomx ping --json
{"error":{"code":"unavailable.not_running","message":"No daemon at the selected socket","data":{"socket":"/run/user/1000/venom/venomx.sock"}}}

Detach and reattach:

$ venomx new --name work           # leader d detaches
[venomx: detached from s0 (work)]
$ venomx attach                    # most recent session
$ venomx new -A --name work        # attach to "work" if it exists, else create it

Under venom-os the supervisor runs:

$ venomx serve --json              # persistent, as today

A human typing venomx then simply gets a new session on that daemon. Exiting the last shell removes the session but the daemon keeps running, because it was started persistent.

2. Who decides the lifetime

A daemon fixes its lifetime when it starts (RFC-0009 section 4) and a running daemon always wins. This RFC states who may start one, and how:

Starter How it starts the daemon Lifetime
A human at a terminal venomx, venomx new (section 3) on-demand
A supervisor (venom-os, systemd, CI) venomx serve persistent
An owner script opted in by config session create with daemon.startup = "on-demand" (RFC-0009) on-demand
An agent, MCP, venom, any other product Never Uses the running daemon or gets unavailable.not_running (6)

There is no global “mode” toggle. The config keys only choose whether the two automatic starters are allowed:

Key Default Meaning
daemon.startup "explicit" Unchanged from RFC-0009: whether owner-opted session create may start a daemon
daemon.interactive_start true Whether venomx / venomx new may start a daemon. venom-os MAY set it to false so a stray start can never shadow its supervised daemon.

Both are restart-only startup policy, read by the client only after a connect reports a missing daemon, exactly as in RFC-0009 section 1.

serve gains an optional origin parameter (serve default, interactive, session_create) that the launcher sets. It is recorded for doctor and server.get only and never grants anything.

venom MUST NOT start a venomx daemon, including through venomx new. With no daemon it runs with ui.chrome = .own (RFC-0016 section 10).

3. Interactive local commands

Three RFC-0011 local commands, all interactive = true (they need a terminal on stdin and stdout, fail unsupported.terminal (10) otherwise, and are left out of MCP and the skill):

Command Params Does
new optional name, cwd, attach_existing (-A), detached (-d), and trailing argv Ensure a daemon (section 4), session.create, window.create with argv or the default shell, then attach unless -d
(no command) none Same as new with no params
attach selector now optional With no selector, attach to the most recently attached session, else the most recently created one. Never starts a daemon and never creates anything.
  • Default shell. $SHELL if it is an absolute path to an executable, else the user’s login shell from the password database, else /bin/sh. It is started as a login shell (argv[0] prefixed with -) like tmux: window.create with program set to the shell’s path and argv set to - plus its basename (RFC-0013 §2), so /bin/zsh runs as -zsh.
  • Default name. Without --name, the session is named after the basename of the working directory (cwd, else the caller’s), truncated to 128 UTF-8 bytes; / gives root. So cd ~/src/venomx && venomx new -A reattaches that project’s session.
  • -A. If a session with exactly that name exists, attach to it instead of creating. Names are not unique (RFC-0007): with more than one match new -A fails conflict.ambiguous_name (5) and lists the IDs.
  • Panes and sessions created by new use on_exit = "remove" (section 5), and the session uses on_empty = "remove".
  • No new daemon verb. new sends session.create and window.create, then performs attach, through the same generated client as the CLI. If window.create fails, new removes the session it just created (best effort) and reports the original error.
  • The attach-or-create alternative that RFC-0009 rejected is replaced by this composition; RFC-0009’s other rejections stand: wire requests, attach, MCP and read verbs never start a daemon.

The last-used session is client-side state in $XDG_STATE_HOME/venomx/last-session, written on attach, holding the daemon’s epoch and the session ID. A stale epoch is ignored.

4. Starting the daemon from new

new is added to RFC-0009 section 2’s eligible commands with these rules:

  1. Owner intent is implicit. new behaves as if VENOMX_TOKEN=@owner were set: after readiness it reads the owner file under RFC-0005 section 3.7. A human does not type a token.
  2. Managed-context guard, unchanged. If VENOMX_PRINCIPAL or VENOMX_PANE is present, even empty, new MUST fail denied.startup_context (4) before connecting. This also refuses nesting (as tmux refuses inside $TMUX). --nested skips the guard for nesting only when a daemon is already running and the caller presents its own token; it never starts a daemon or reads the owner file. When no daemon is running, a manager’s marker above the caller (RFC-0009 section 2) also fails denied.startup_context, before launching.
  3. Terminal requirement. Stdin and stdout must be terminals. Agent shells (Claude Code’s Bash tool, Codex exec) have no TTY. This is accident prevention, not an identity boundary, like rule 2 (RFC-0009 section 2).
  4. Launch, readiness, deadlines and lock arbitration are exactly RFC-0009 section 3, with --lifetime on-demand --origin interactive. A persistent daemon that wins the race is used as-is.
  5. daemon.interactive_start = false makes new fail unavailable.not_running (6) when no daemon runs.

5. Exited-pane removal (on_exit)

window.create, pane.split, pane.float and pane.popup (RFC-0016) gain an optional parameter:

Param Choices Default
on_exit keep, remove keep for the verbs (compatible with RFC-0007); remove for pane.popup

session.create gains on_empty (keep default, remove).

Semantics with on_exit = "remove":

  1. When the pane’s process exits, the daemon records the exit as today (pane.exited), then removes the pane in a following commit (pane.removed, reason on_exit). Streams close per RFC-0006 with the exit status, so attached clients still see why it ended.
  2. If that leaves its window with no panes, the window is removed (window.removed, reason empty). Windows always cascade; an empty window has nothing to show, and there is no on_empty for windows.
  3. If that leaves a session with on_empty = "remove" with no windows, the session is removed (session.removed, reason empty).
  4. Removal of the last session by this cascade counts as explicit removal for RFC-0009 section 4, so an on-demand daemon then stops. A persistent daemon stays.

Rules:

  • on_exit is fixed at creation and shown in the pane’s snapshot entity.
  • Panes started by RFC-0015 run keep their own result-retention rules and ignore on_exit; agents need results after exit.
  • A pane removed by on_exit is not archived by RFC-0010 recovery. A pane restored from an archive keeps its recorded on_exit.
  • The built-in key table (RFC-0016 section 7) passes on_exit = "remove" for its split and new-window bindings, so everything a human creates by hand behaves like tmux. Products choose explicitly; venom SHOULD use keep for agent panes and remove for helper popups.

6. Leaving the client

When a session the client shows is removed, the client attaches to the most recent other session if one exists (tmux’s detach-on-destroy off behaviour is config attach.on_session_end = "next"), else it exits 0 after printing the one-line message in section 1. Default: "exit".

7. Errors

Code Exit When
unsupported.terminal 10 new or bare venomx without a terminal (RFC-0011)
denied.startup_context 4 Managed-context markers present (section 4)
conflict.ambiguous_name 5 new -A matches several sessions
not_found.session 3 Selector-less attach with no session
unavailable.not_running 6 No daemon and starting is not allowed

8. Conformance

  1. With no daemon, venomx in a terminal starts an on-demand daemon, opens a login shell and attaches; exit removes pane, window and session and stops the daemon.
  2. The same against a persistent daemon removes the session but leaves the daemon running.
  3. venomx new with VENOMX_PANE set fails denied.startup_context without connecting; with --nested and a token against a running daemon it attaches.
  4. venomx new with stdin not a TTY fails unsupported.terminal.
  5. venomx attach, session list, mcp and venom never start a daemon.
  6. Two concurrent venomx starts produce one daemon (RFC-0009 section 3).
  7. A pane with on_exit = keep survives its exit and can be captured; a run pane ignores on_exit.
  8. new -A --name work attaches when one work exists, creates when none, fails with two.
  9. doctor shows lifetime on-demand, origin interactive.

Agent-contract impact

  • Verbs. No new daemon verbs. window.create, pane.split, pane.float, pane.popup gain on_exit; session.create gains on_empty; serve gains origin. New local commands new and the bare command; attach selector becomes optional. All in the generated CLI, docs and completions; interactive commands stay out of MCP and skill (RFC-0011).
  • --json and exit codes. Section 7, on RFC-0002’s mapping.
  • Events. pane.removed, window.removed and session.removed gain reasons on_exit and empty.
  • doctor reports latched lifetime, origin, the startup keys and a stale last-session file. config: daemon.interactive_start, attach.on_session_end.
  • Grants. Unchanged. new authorizes as the owner through the owner file; every verb it sends is checked by the daemon. Agents keep no path to owner rights (section 4 rules 2–3).
  • Headless. Unchanged; the new entry points are for humans only.
  • OSC 7501 / 133. No change.

Alternatives

  • Do nothing. venomx stays agent-first and humans keep using tmux.
  • A global mode toggle (mode = standalone | system). Two daemons could disagree about the mode; tying lifetime to the starter needs no coordination and matches RFC-0009’s latching.
  • Default daemon.startup = "on-demand" for every command. Lets scripts and agents start daemons and bootstrap owner rights by accident, which RFC-0009 rejected for good reason.
  • A daemon-side session.new_with_shell verb. A second mutation path with its own authorization; the client-side composition reuses the existing verbs.
  • Remove exited panes always. Breaks agents that capture output after a command ends.
  • Socket activation (systemd). Possible later for venom-os; it is a supervisor choice and does not need protocol changes.

Open questions

All three were resolved by the maintainer on 2026-11-07 by accepting the proposals below. None remains open within this RFC’s scope.

  1. Short aliases — resolved: no command aliases in v1. The generated CLI keeps one name per command (RFC-0011), and the README suggests shell aliases (alias va='venomx attach').
  2. Default session names — resolved: new names the session after the working directory’s basename (section 3). IDs stay s0, s1.
  3. Empty windows — resolved: an on_exit cascade always removes the emptied window (section 5); no product needs a placeholder window.