Venom
RFC-0013venom-spec

Per-request working directory and environment for pane creation

Field Value
Status Accepted
Accepted 2026-11-07, explicitly approved by the maintainer
Authors commixon (commixon)
Created 2026-11-07
Linear COM-247; implementation COM-248; program amendment COM-305
Supersedes None
Superseded by None

History

  • 2026-11-10 (venom-org/venom-spec#144, venom-org/venomx#147; normative, narrow compatible change; accepted by venom-run (unattended, delegated by the maintainer) on 2026-11-10): §7’s events item now allows RFC-0007 §5’s argv0, the basename of argv[0] cut to 255 bytes, in pane.created data. It is the only value derived from argv/program that an event carries. It is never program, and never the directory part of argv[0] (anything up to its last /), the bytes past 255, or another argv element. The redaction of cwd, env, env_unset, the built envp and child tokens, and §4’s retention rules, are unchanged.

  • 2026-11-08 (COM-305, normative, compatible addition; accepted through the COM-305 PR review): window.create and pane.split gain an optional program parameter (§2), so the file to execute can differ from argv[0]. §5 validates it, §6 resolves it in place of argv[0], §10 adds cases 27 to 29. Requests without it behave as before. RFC-0017 §3 uses it to start the default shell with a --prefixed argv[0].

Summary

Add three optional, typed parameters to venomx window.create and pane.split: cwd (an absolute directory for the new pane’s process), env (variables to set or replace) and env_unset (variables to remove). This RFC defines how the child’s working directory and environment are built, how the executable is found on the final PATH, how inputs are validated and bounded, and how reserved identity variables and credentials are handled under RFC-0005. It keeps direct argv execution and the prepare/exec rollback of RFC-0007 §5. Omitting all three parameters keeps today’s behaviour, apart from the small hardening listed in §9. COM-248 implements it in venomx.

Motivation

RFC-0007 §5 runs every pane in the daemon’s own working directory and environment, and defers per-request overrides. That is enough for a human who runs a shell and types cd, but not for the callers venomx exists for:

  • venom starts agents in panes. Each agent needs to run in its thread’s worktree, and RFC-0005 §6.3 says venom passes the agent’s VENOM_TOKEN “through the spawn verb’s environment parameter”. That parameter does not exist yet, so cross-daemon delegation cannot be implemented as specified.
  • Agents and scripts that call window create or pane split want make test in a project directory with a project PATH. Today they must wrap every command in /bin/sh -c 'cd … && exec …', which reintroduces shell quoting and hides exec failures behind a shell that exits 127.
  • venom-desk and the attach client (COM-223) want “split here” to open in the focused pane’s directory, which they learn from OSC 7.

Without this RFC each of these grows its own shell wrapper, the RFC-0005 delegation path stays unimplementable, and the credential rules for explicitly passed variables stay unwritten. COM-248 is blocked on it.

Design

1. Examples

A headless daemon is running (venomx serve). The human opts into owner authority as in RFC-0007 §1. IDs assume a fresh daemon.

VENOMX_TOKEN=@owner venomx session create --name build --json
VENOMX_TOKEN=@owner venomx window create --session s0 --cwd /tmp \
  --env '[{"name":"GREETING","value":"hello"},{"name":"LANG","value":"C.UTF-8"}]' \
  --env-unset '["HISTFILE"]' \
  --argv '["/bin/sh","-c","pwd; printf \"%s %s\\n\" \"$GREETING\" \"${HISTFILE-unset}\"; exec sleep 1000"]' \
  --json
venomx pane capture --pane p0 --json

The responses:

{"session":"s0","name":"build"}
{"window":"w0","session":"s0","pane":"p0","principal":"g1","pid":41007}

and the first two rows of the capture’s text are /tmp and hello unset. The result shapes are RFC-0007’s, unchanged: neither cwd nor env is echoed back.

The CLI passes --cwd verbatim; it does not resolve relative paths against the caller’s directory. From a shell, use --cwd "$PWD":

$ VENOMX_TOKEN=@owner venomx pane split --pane p0 --axis cols --cwd . --argv '["/bin/sh"]' --json
{"error":{"code":"invalid_params.cwd","message":"cwd must be an absolute path","data":{"param":"cwd","reason":"relative"}}}
$ echo $?
2
$ VENOMX_TOKEN=@owner venomx pane split --pane p0 --axis cols --cwd /nonexistent/dir --argv '["/bin/sh"]' --json
{"error":{"code":"not_found.cwd","message":"working directory not found","data":{}}}
$ echo $?
3
$ VENOMX_TOKEN=@owner venomx pane split --pane p0 --axis cols --env '[{"name":"PATH","value":"/opt/empty"}]' --argv '["sh"]' --json
{"error":{"code":"unavailable.spawn","message":"executable not found","data":{"reason":"not_found"}}}
$ echo $?
6
$ VENOMX_TOKEN=@owner venomx pane split --pane p0 --axis cols --env '[{"name":"VENOMX_TOKEN","value":"x"}]' --argv '["/bin/sh"]' --json
{"error":{"code":"invalid_params.env","message":"reserved environment variable","data":{"param":"env.0.name","reason":"reserved"}}}
$ echo $?
2

venom starting an agent pane in a worktree, on the wire (RFC-0002 NDJSON, after hello). The VENOM_TOKEN value is illustrative, not a real token:

{"id":7,"verb":"pane.split","params":{"pane":"p0","axis":"rows","argv":["claude"],"cwd":"/home/u/src/app-com-12","env":[{"name":"VENOM_TOKEN","value":"vnt1.venom.g7.aWxsdXN0cmF0aXZlIHZlbm9tIHRva2VuIG9ubHkhISE"},{"name":"VENOM_PRINCIPAL","value":"g7"}],"grants":[{"right":"read","scope":"session:s0"},{"right":"write","scope":"self"}]}}
{"id":7,"ok":{"pane":"p1","window":"w0","session":"s0","principal":"g2","pid":41020}}

The agent’s process sees VENOM_TOKEN and VENOM_PRINCIPAL exactly as given, plus the VENOMX_TOKEN, VENOMX_PRINCIPAL, VENOMX_PANE and VENOMX_SOCKET that venomx sets for pane p1 (§4).

2. Schema

All additions go in the venomx RFC-0004 format-1 manifest, on both window.create and pane.split. They are optional and have no defaults, so absence is distinguishable from an empty value. CLI flags follow RFC-0004 §4: --cwd, --env and --env-unset, the last two taking one JSON value each.

.{
    .{ .name = "cwd", .summary = "Absolute working directory for the new process; default: the daemon's working directory", .type = .{ .kind = .string } },
    .{
        .name = "env",
        .summary = "Environment variables to set or replace in the new process, applied after env_unset",
        .type = .{ .kind = .array, .items = .{ .kind = .object, .fields = .{
            .{ .name = "name", .summary = "Variable name", .type = .{ .kind = .string }, .required = true },
            .{ .name = "value", .summary = "Variable value", .type = .{ .kind = .string }, .required = true },
        } } },
    },
    .{ .name = "env_unset", .summary = "Environment variable names to remove from the inherited environment", .type = .{ .kind = .array, .items = .{ .kind = .string } } },
    .{ .name = "program", .summary = "File to execute, resolved like argv[0] would be; argv is then passed as given, so argv[0] may be any name, such as -sh for a login shell; default: argv[0]", .type = .{ .kind = .string } },
}

program separates the file to execute from the name the program sees, as execve does. It grants nothing new: a caller with spawn can already run any file under any name through a symlink. With program set, argv is still required and nonempty and its first entry is still nonempty. A login shell is:

$ VENOMX_TOKEN=@owner venomx window create --session s0 --program /bin/sh --argv '["-sh"]' --json

Format 1 expresses all three without unions, maps or new kinds, so no shared generator work is required (§8). Format 1 has no string-length, pattern or array-length constraints, so the handler enforces §5’s rules and reports them as product errors. The manifest’s examples for these verbs MUST use benign values only and MUST NOT include any *_TOKEN variable, so generated docs, man pages and skill never show one.

Results, events and the RFC-0007 §4 window object are unchanged. The daemon does not report a pane’s cwd or environment anywhere (§7).

3. Working directory

  • Absent: the child inherits the daemon’s working directory, as in RFC-0007. pane.split does not inherit the source pane’s directory; a client that wants that passes it explicitly (for example from the source pane’s OSC 7 observation). See Open question 1.
  • Present: the value MUST be an absolute path (first byte /), 1–4095 bytes, with no NUL. Anything else fails invalid_params.cwd (§6) before any filesystem access. There is no relative form, because the daemon cannot know the caller’s directory, and the generated CLI passes the string verbatim.

After authorization and the earlier checks (§5, steps 2–5), the daemon resolves the path with realpath. ENOENT or ENOTDIR fails not_found.cwd. Any other failure (EACCES, ELOOP, …) fails unavailable.cwd. A resolved path that is not a directory fails not_found.cwd. Resolution happens in the daemon, before preparation, so these failures create nothing.

The child calls chdir(<resolved path>) after fork and before exec. If that fails (the directory was removed or its permissions changed after resolution), the child reports it through the exec-status pipe as a cwd-phase failure, and the request fails unavailable.cwd with reason = "changed" (ENOENT, ENOTDIR), "permission" (EACCES) or "other", after RFC-0007’s rollback.

When cwd is present, the daemon sets PWD to the resolved path, unless the request names PWD in env or env_unset, in which case the caller’s choice wins. When cwd is absent, PWD is whatever the base environment holds (§4), as today.

4. Environment

The child’s environment is built in this order, entirely in the daemon before fork:

  1. Base. Start from the daemon’s own environment, as captured at serve start.

  2. Strip. Remove every base variable whose name

    • ends in _TOKEN (RFC-0005 §3.6, unchanged);
    • ends in _PRINCIPAL (stale informational identity of whoever started the daemon);
    • starts with VENOMX_;

    and every base variable whose value contains a token-shaped substring: a match of vnt1\.[a-z][a-z0-9-]{0,31}\.(owner|unknown|g[1-9][0-9]*)\.[A-Za-z0-9_-]{43} (RFC-0005 §3.6 token syntax, with RFC-0002 tool names).

  3. Unset. Remove each name in env_unset. Unsetting a name that is absent is not an error.

  4. Set. For each env entry in order, set the name to the value, replacing any existing value.

  5. Daemon variables. Set, overriding anything above:

    Variable Value
    VENOMX_TOKEN The new pane principal’s token (RFC-0005 §3.6)
    VENOMX_PRINCIPAL The new pane principal’s ID
    VENOMX_PANE The new pane’s ID
    VENOMX_SOCKET The absolute path of the socket this daemon listens on
    PWD Only as described in §3

VENOMX_SOCKET is new for every spawn, with or without these parameters. It makes the child’s generated CLI reach the daemon that issued its VENOMX_TOKEN, so the token is never presented to a different venomx daemon because of a stale or caller-chosen socket path.

The daemon never sees, and the generated CLI MUST NOT forward, the requesting client’s own environment. The only caller-controlled inputs are env and env_unset. This is what RFC-0005 §3.6 calls “a token passed explicitly in the spawn verb’s environment parameter”.

Reserved names. Any name starting with VENOMX_ is reserved. Naming one in env or env_unset fails invalid_params.env with reason reserved. The daemon alone controls venomx identity and routing in its children.

Credentials in env. For each env entry, the daemon checks the value:

Value Outcome
Exactly @owner, with a name ending in _TOKEN Rejected: owner_credential. @owner makes a child’s CLI read an owner file (RFC-0005 §3.7).
Contains a token-shaped substring whose principal is owner Rejected: owner_credential. Owner credentials never enter children, whatever the variable.
Contains a token-shaped substring for tool venomx Rejected: credential. The pane gets its own venomx principal; passing another venomx token would let the child act as a different principal.
Contains another tool’s token-shaped substring Accepted only when the entire value is that one token and the name is that tool’s <TOOL>_TOKEN (tool name upper-cased, - replaced by _), for example VENOM_TOKEN for vnt1.venom.…, VENOM_MEM_TOKEN for vnt1.venom-mem.…. Otherwise rejected: credential.
Anything else, including non-Venom secrets such as GITHUB_TOKEN Accepted as given.

An accepted cross-product token is passed through unchanged, and it remains subject to the receiving daemon’s binding and floor rules (RFC-0005 §3.4). venomx does not verify other daemons’ tokens and grants no venomx authority for them. Explicit *_PRINCIPAL variables for other tools (VENOM_PRINCIPAL) are accepted; they are informational and never trusted (RFC-0005 §3.6).

Retention. The daemon MUST NOT retain the explicit env values, the built envp or the child token after the exec-status outcome is known, on success or failure. Buffers that held them MUST be zeroed (with a zeroing primitive the compiler cannot elide) before they are freed. The resolved cwd is not retained either. RFC-0010 checkpoints already exclude environment and cwd, and this RFC adds nothing to them.

5. Validation, limits and order

env and env_unset entries are checked against these rules. Each failure is invalid_params.env with data.param the dotted path (env.3.name, env.3.value, env_unset.0, or env / env_unset for whole-array limits) and data.reason:

Rule reason
A name matches ^[A-Za-z_][A-Za-z0-9_]*$ and is 1–255 bytes syntax
A value is at most 65536 bytes size
No name or value contains NUL (JSON \u0000) nul
env and env_unset each have at most 256 entries count
The sum over env of name bytes + value bytes + 2 is at most 262144 size
A name appears at most once across env and env_unset together duplicate
No name starts with VENOMX_ reserved
Values pass the credential table in §4 credential, owner_credential

Values may contain any other characters, including newlines and other control characters; JSON guarantees they are valid UTF-8, so non-UTF-8 values cannot be passed. Names are case-sensitive. An empty array is the same as an absent parameter.

The handler processes a request in this order. The first failing step determines the error, so conformance results are deterministic:

  1. RFC-0004 schema validation (types, unknown fields), in proto.
  2. spawn authorization on the session (window.create) or source pane (pane.split), before lookup (RFC-0005 §7).
  3. Target lookup (not_found.*), then conflict.pane_exited and the RFC-0012 conflict.* checks for pane.split.
  4. grants delegation check (denied.delegation, RFC-0005 §6.2).
  5. Syntactic checks of argv, program, cwd (§3) and env / env_unset (this section), in that parameter order. An empty program or a NUL in it fails invalid_params.argv with data.param = "program".
  6. cwd resolution (§3). No filesystem access happens before this step, so a caller without spawn learns nothing about host paths.
  7. RFC-0007 preparation, child start, chdir, executable search (§6) and the exec-status outcome, with RFC-0007’s rollback on failure.

The built argv plus envp may still exceed the OS limit (E2BIG); that is reported at step 7 as unavailable.spawn with reason too_large.

6. Executable resolution

RFC-0007 says the daemon resolves the executable “using its environment”. This RFC makes that precise, and makes it use the child’s final environment, so a caller that sets PATH gets the program it named. Below, the search name is program when the request gives it and argv[0] otherwise:

  1. If the search name contains /, the child calls execve(<search name>, argv, envp) once. A relative path such as ./run.sh therefore resolves against the child’s working directory after §3’s chdir.
  2. Otherwise let P be the final environment’s PATH, or, if PATH is absent, the platform’s confstr(_CS_PATH) value read at serve start. Split P on :. Skip empty and relative entries. POSIX treats them as the current directory, which here is a caller-chosen directory, so skipping them keeps a caller’s cwd from silently changing which program runs. For each remaining entry, the candidate is <entry>/<search name>; a candidate longer than 4095 bytes is skipped.
  3. The child calls execve(candidate, argv, envp) for each candidate in order. On ENOENT, ENOTDIR, ELOOP, ENAMETOOLONG or EACCES it tries the next one, remembering whether any EACCES occurred. Any other error stops the search.
  4. argv[0] is passed to the program as given, never rewritten to the resolved path or to program.
  5. There is no shell fallback. On ENOEXEC the child MUST NOT retry through /bin/sh as execvp does. Direct argv execution means a script without a valid #! line fails.

The daemon MUST build envp and the candidate list before fork. The child performs only async-signal-safe calls (chdir, execve, write to the exec-status pipe, _exit) and allocates nothing.

The exec-status pipe carries a phase (cwd or exec) and an errno. An exec-phase failure maps to unavailable.spawn with data.reason:

Condition reason
No candidate existed, and no EACCES was seen not_found
The search ended with at least one EACCES and no success permission
ENOEXEC exec_format
E2BIG too_large
EAGAIN, ENOMEM, EMFILE, ENFILE, or PTY/process creation failure before exec resource
Anything else other

Every cwd or exec failure follows RFC-0007 §5 exactly: terminate and reap the child, close its PTY, revoke the prepared principal, discard staged resources and events. A program that execs and then exits 127 is a successful creation followed by pane.exited, as before.

7. Grants, redaction and threat notes

Grants. No new right. spawn on the target (RFC-0007 §5) covers cwd, env and env_unset. A principal with spawn can already run any program, which can cd anywhere the UID can and set any variable, so requiring files on a path: scope for cwd would restrict the convenient form while leaving ["/bin/sh","-c","cd /x && …"] allowed. Grants are not a sandbox (RFC-0005 §12). When OS-backed containment arrives (COM-262), cwd MUST be checked against it; this RFC’s step 6 is where that check goes.

Explicit environment cannot raise venomx authority: VENOMX_* is reserved, venomx tokens are rejected, and the new pane’s own principal is attenuated by RFC-0005 §6.2 regardless of its environment. A caller can only pass on credentials for other tools that it already holds, which is the delegation RFC-0005 §6.3 requires.

Redaction. Explicit environment values, the built envp, child tokens and cwd strings MUST NOT appear in:

  • results and error messages or data (errors name only data.param and data.reason; validation messages are fixed strings);
  • events (pane.created, principal.created and all others carry no params, RFC-0005 §9). The one exception is RFC-0007 §5’s argv0 in pane.created, a basename derived from argv[0]. It is never program, and never the directory part of argv[0] (anything up to its last /), the bytes past 255, or another argv element. It is not one of the params this list redacts;
  • audit records (RFC-0005 §10 already excludes params);
  • doctor output, daemon logs and debug traces, including any request logging, which MUST omit these three params entirely;
  • RFC-0010 checkpoints;
  • generated docs, man pages, completions and skill, through the manifest example rule in §2.

Names of explicit variables are not secret, but they are omitted too, so redaction never depends on guessing which names are sensitive.

# Threat Mitigation Residual
E1 Caller injects its own or another venomx principal’s token VENOMX_* reserved; venomx token-shaped values rejected (§4) None
E2 Owner credential reaches a child (@owner, owner token in any variable, inherited from the daemon’s environment) Rejected in env; stripped from the base (§4) A non-vnt1 encoding of the owner secret is not detectable
E3 Child presents its venomx token to another venomx daemon VENOMX_SOCKET set by the daemon and reserved (§4) The child itself may override it in its own shell
E4 Caller-chosen cwd changes which program runs Empty and relative PATH entries skipped; no shell fallback (§6) argv[0] containing / is deliberately cwd-relative
E5 Credentials leak through venomx observability §7 redaction list; conformance scans (§10) Other same-UID processes can read /proc/<pid>/environ of the child (RFC-0005 §12)
E6 Credentials leak on the caller’s side Wire and MCP carry values only in the request CLI --env values are visible in the client’s argv and shell history; MCP arguments are visible to the MCP host and its transcripts. skill warns about both.
E7 Filesystem probing by unauthorized callers cwd resolution after spawn authorization (§5) A spawn holder can probe paths, as any program it starts could

8. Errors

New product errors, declared on both verbs:

Code Exit When Data
invalid_params.cwd 2 §3 syntax {"param":"cwd","reason":"relative"|"size"|"nul"}
invalid_params.env 2 §5 rules {"param":string,"reason":string}, reasons as in §5
not_found.cwd 3 cwd does not name an existing directory {}
unavailable.cwd 6 cwd exists but cannot be resolved or entered {"reason":"permission"|"changed"|"other"}

Changed: unavailable.spawn gains an optional data.reason string with the choices in §6. Its RFC-0007 meaning and exit 6 are unchanged; clients that ignore data see no difference. Its error-data descriptor in the manifest becomes an object with that one optional field.

Also, invalid_params.argv now covers program (§5), with data.param naming it; a request without program sees no difference.

Unchanged: invalid_params.size, .grant_scope; denied.spawn and denied.delegation (RFC-0005 data); not_found.session / .pane; conflict.pane_exited; the RFC-0012 conflict.* codes; unavailable.shutdown.

9. Compatibility

A request without cwd, env and env_unset behaves as RFC-0007 specifies, with these changes that apply to every spawn:

  1. The daemon additionally strips inherited *_PRINCIPAL, VENOMX_* and token-shaped variables, and sets VENOMX_SOCKET (§4).
  2. PATH search skips empty and relative entries and never falls back to /bin/sh (§6). RFC-0007 did not specify either point.
  3. unavailable.spawn may carry data.reason (§8).

These are compatible additions to RFC-0007 §5 under PROCESS.md “Changing an RFC”. On acceptance, RFC-0007’s History records that this RFC defines the per-request cwd and environment its §5 deferred. pane.relaunch (RFC-0010) is not changed by this RFC (Open question 2, COM-265).

10. Conformance matrix

COM-248 MUST pass each case against a real headless daemon with real PTYs and processes, through both the CLI and MCP. The probe is a child such as ["/bin/sh","-c","pwd -P > \"$1\"; env >> \"$1\"; exec sleep 1000","probe","<file>"] that writes its physical working directory and full environment to a file under the test’s temporary directory. A canary is a random string unique to the run, used as a variable value. “No side effects” means no new session, window, pane, principal, PTY, child process or event, and no topology change.

# Setup Action Expected
1 Daemon started in directory D with base variable BASE=1 Probe with no new params cwd D; BASE=1; VENOMX_TOKEN, VENOMX_PRINCIPAL, VENOMX_PANE, VENOMX_SOCKET set to the new pane’s values
2 Base holds X_TOKEN, VENOM_PRINCIPAL, VENOMX_DEBUG, FOO=<owner-token-shaped>, VENOMX_TOKEN=@owner Probe None of them in the child; doctor counts them as stripped, by rule, without names or values
3 Temp dir T, symlink L → T Probe with cwd T, then L cwd is T (physical) both times; PWD = T
4 — Probe with cwd T and env PWD=/elsewhere; and with env_unset ["PWD"] PWD=/elsewhere; PWD absent
5 Base BASE=1, LANG=C env sets BASE=2, NEW=canary; env_unset ["LANG","ABSENT"] BASE=2, NEW=canary, no LANG; absent name is not an error
6 — --cwd ., --cwd "", 4096-byte cwd, cwd with NUL invalid_params.cwd with relative, relative, size, nul; no side effects
7 — cwd missing; cwd is a regular file; cwd inside a mode-000 directory not_found.cwd, not_found.cwd, unavailable.cwd (permission); no side effects
8 Seam removes the directory between resolution and chdir Probe with that cwd unavailable.cwd (changed); RFC-0007 rollback: no pane, PTY, principal, child or event
9 Directory B with executable tool, and a different tool in the daemon’s PATH argv ["tool"] with env PATH=B B’s tool runs
10 PATH with empty and relative entries; tool only in the cwd argv ["tool"], cwd = that directory unavailable.spawn (not_found); with argv ["./tool"] it runs
11 env_unset ["PATH"] argv ["sh"] Resolved through confstr(_CS_PATH)
12 Non-executable candidate first, executable later in PATH; only non-executable candidates; script without #!; missing program Each spawn Later one runs; permission; exec_format (no shell fallback); not_found. Each failure leaves no side effects
13 — Name 1X, A-B, 256-byte name; 65537-byte value; NUL in name and value; 257 entries; 262145 total bytes; same name twice; same name in env and env_unset invalid_params.env with data.param and reasons syntax, syntax, syntax, size, nul, nul, count, size, duplicate, duplicate; no side effects
14 — env or env_unset naming VENOMX_TOKEN, VENOMX_SOCKET, VENOMX_ANYTHING reserved; no side effects
15 Caller holds a venomx token for principal g3 env VENOMX_TOKEN=<g3 token>; OTHER=<g3 token> reserved; credential
16 — VENOM_TOKEN=@owner; VENOM_TOKEN=vnt1.venom.owner.<43>; NOTE=prefix vnt1.venom-mem.owner.<43> owner_credential for each; no side effects
17 A real venom token for agent g7 env VENOM_TOKEN=<it>, VENOM_PRINCIPAL=g7, GITHUB_TOKEN=canary Child sees all three exactly; child’s venom whoami (or the conformance stand-in daemon) resolves g7 capped by its floor
18 — VENOM_MEM_TOKEN=<venom token>; VENOM_TOKEN=<token> trailing credential for both
19 Caller without spawn; caller with spawn self on a pane it does not own Request with invalid env and a missing cwd denied.spawn, identical for existing and missing targets; no filesystem access (seam counts realpath calls: 0)
20 Caller with spawn Request with grants beyond its own plus invalid env denied.delegation (step 4 precedes step 5)
21 Exited source pane pane split with a valid cwd conflict.pane_exited; no filesystem access
22 Injected failure at each preparation step after cwd resolution Spawn with env and cwd RFC-0007 rollback; no ghost pane; IDs may be consumed but never reused
23 Huge argv plus env exceeding ARG_MAX but within §5 Spawn unavailable.spawn (too_large); no side effects
24 Canary values in every env entry and in cwd (a directory whose name contains a canary) All cases above, plus doctor, events --json from before the run with replay, audit log, daemon stderr and log files, every error reply, generated docs and skill, and an RFC-0010 checkpoint where recovery is enabled No canary, no vnt1. string from a request, and no explicit variable name appears anywhere except in the probe’s own output file
25 Seam inspects daemon memory after cases 17 and 22 Search freed and live heap for the canaries and child tokens Not found
26 Daemon started with --socket S Probe VENOMX_SOCKET=S; the child’s venomx whoami --json returns its own pane principal
27 Executable tool in directory B program B’s absolute path, argv ["-tool"]; program tool, argv ["-tool"] with env PATH=B; the same for pane.split B’s tool runs each time with argv[0] exactly -tool
28 Executable tool in cwd C program ./tool, argv ["-tool"], cwd C; program tool with PATH of empty and relative entries; program missing; argv ["-tool"] with no program The first runs; the others unavailable.spawn (not_found) with no side effects
29 — program ""; program with NUL; valid program with argv [] invalid_params.argv with data.param program, program, argv; no side effects

Agent-contract impact

  • Verbs/schema: three optional parameters on window.create and pane.split (§2), expressible in RFC-0004 format 1 with no generator change. CLI, MCP, docs and completions are generated from the one manifest as usual. No new verbs, no local dispatch.
  • JSON/exits: standard. New errors in §8; unavailable.spawn gains optional data.reason.
  • Events: no change. No event carries cwd, environment names or values (§7).
  • Skill: the generated skill MUST say that --cwd takes an absolute path (use --cwd "$PWD"), that env is applied after env_unset on top of the daemon’s environment, that the executable is found on the child’s final PATH with no shell, that VENOMX_* is reserved, and that credentials passed with --env are visible in the caller’s argv and history and in MCP transcripts, so pass only what the child needs. It MUST NOT show token values.
  • Doctor: add spawn.path (warn when the base environment has no PATH, or its PATH has empty or relative entries that §6 skips), spawn.base_env (info: counts of base variables stripped by each §4 rule) and spawn.cwd (warn when the daemon’s own working directory no longer exists). Add counters of invalid_params.env by reason, not_found.cwd, unavailable.cwd by reason and unavailable.spawn by reason. Doctor never reports paths, names or values.
  • Config: none. Defaults are the daemon’s cwd and environment; there is no configured default environment (Open question 3, COM-266).
  • Grants: spawn on the target, unchanged; no files check for cwd (§7). RFC-0005 §3.6’s explicit-environment exception is now defined exactly (§4).
  • Headless/OSC: fully headless. OSC 7 observations are an input a client may use to choose cwd; they are never trusted by the daemon.

Alternatives

  • One env array with optional value meaning unset: fewer flags, but a missing value from a typo would silently remove a variable. Separate env_unset makes removal explicit.
  • A string map ({"FOO":"bar"}): natural JSON, but format 1 has no arbitrary maps, and ordering would be unspecified.
  • Relative cwd resolved by the CLI: needs per-parameter CLI behaviour that RFC-0004 deliberately does not have, and would differ between the CLI and MCP. Absolute-only is the same everywhere.
  • Require files on path: for cwd: security theatre while spawn can run a shell that changes directory (§7). Revisit with OS-backed containment (COM-262).
  • Resolve the executable with the daemon’s PATH: surprising when a caller sets PATH for the child, and gives two different meanings of PATH in one request.
  • A sensitive annotation in RFC-0004 for generic redaction: would let shared code redact any marked field, but no shared code logs params today, and the handler rules in §7 are enough. Not required by this design.
  • Allow any explicit token, including venomx’s own: contradicts RFC-0005 §3.6 (“a pane MUST NOT inherit the token of whoever created it”) and would let a caller run a child as an arbitrary venomx principal.
  • Do nothing: every caller keeps writing sh -c 'cd … && exec …', and RFC-0005 §6.3’s venom delegation stays unimplementable.

Open questions

  1. Inheriting the source pane’s directory on split. Resolved: no inheritance. The daemon does not track a pane’s live directory (/proc/<pid>/cwd reflects only the root process, and OSC 7 is an untrusted observation), so clients pass cwd explicitly.
  2. pane.relaunch (RFC-0010). Deferred to COM-265. It creates a process with the same barrier and could take the same three parameters. Until then it keeps RFC-0010’s behaviour.
  3. Clean or configured base environment (an env_inherit=false flag, or a spawn.env config profile). Deferred to COM-266. Until then, env_unset covers specific variables.
  4. Limits. Resolved: the numbers in §5 (256 entries, 64 KiB per value, 256 KiB total, 4095-byte cwd) stand. They sit well inside Linux’s MAX_ARG_STRLEN and the default 1 MiB frame.

The maintainer accepted this RFC and its resolved/deferred questions on 2026-11-07. RFC-0007’s History records that this RFC now defines the per-request cwd and environment that RFC-0007 §5 deferred.