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 ofargv[0]cut to 255 bytes, inpane.createddata. It is the only value derived fromargv/programthat an event carries. It is neverprogram, and never the directory part ofargv[0](anything up to its last/), the bytes past 255, or another argv element. The redaction ofcwd,env,env_unset, the builtenvpand 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.createandpane.splitgain an optionalprogramparameter (§2), so the file to execute can differ fromargv[0]. §5 validates it, §6 resolves it in place ofargv[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--prefixedargv[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 createorpane splitwantmake testin a project directory with a projectPATH. 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.splitdoes 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 failsinvalid_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:
-
Base. Start from the daemon’s own environment, as captured at
servestart. -
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). - ends in
-
Unset. Remove each name in
env_unset. Unsetting a name that is absent is not an error. -
Set. For each
enventry in order, set the name to the value, replacing any existing value. -
Daemon variables. Set, overriding anything above:
Variable Value VENOMX_TOKENThe new pane principal’s token (RFC-0005 §3.6) VENOMX_PRINCIPALThe new pane principal’s ID VENOMX_PANEThe new pane’s ID VENOMX_SOCKETThe absolute path of the socket this daemon listens on PWDOnly 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:
- RFC-0004 schema validation (types, unknown fields), in
proto. spawnauthorization on the session (window.create) or source pane (pane.split), before lookup (RFC-0005 §7).- Target lookup (
not_found.*), thenconflict.pane_exitedand the RFC-0012conflict.*checks forpane.split. grantsdelegation check (denied.delegation, RFC-0005 §6.2).- Syntactic checks of
argv,program,cwd(§3) andenv/env_unset(this section), in that parameter order. An emptyprogramor a NUL in it failsinvalid_params.argvwithdata.param = "program". cwdresolution (§3). No filesystem access happens before this step, so a caller withoutspawnlearns nothing about host paths.- 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:
- If the search name contains
/, the child callsexecve(<search name>, argv, envp)once. A relative path such as./run.shtherefore resolves against the child’s working directory after §3’schdir. - Otherwise let P be the final environment’s
PATH, or, ifPATHis absent, the platform’sconfstr(_CS_PATH)value read atservestart. 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’scwdfrom silently changing which program runs. For each remaining entry, the candidate is<entry>/<search name>; a candidate longer than 4095 bytes is skipped. - The child calls
execve(candidate, argv, envp)for each candidate in order. OnENOENT,ENOTDIR,ELOOP,ENAMETOOLONGorEACCESit tries the next one, remembering whether anyEACCESoccurred. Any other error stops the search. argv[0]is passed to the program as given, never rewritten to the resolved path or toprogram.- There is no shell fallback. On
ENOEXECthe child MUST NOT retry through/bin/shasexecvpdoes. 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.paramanddata.reason; validation messages are fixed strings); - events (
pane.created,principal.createdand all others carry no params, RFC-0005 §9). The one exception is RFC-0007 §5’sargv0inpane.created, a basename derived fromargv[0]. It is neverprogram, and never the directory part ofargv[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);
doctoroutput, 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:
- The daemon additionally strips inherited
*_PRINCIPAL,VENOMX_*and token-shaped variables, and setsVENOMX_SOCKET(§4). PATHsearch skips empty and relative entries and never falls back to/bin/sh(§6). RFC-0007 did not specify either point.unavailable.spawnmay carrydata.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.createandpane.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.spawngains optionaldata.reason. - Events: no change. No event carries
cwd, environment names or values (§7). - Skill: the generated skill MUST say that
--cwdtakes an absolute path (use--cwd "$PWD"), thatenvis applied afterenv_unseton top of the daemon’s environment, that the executable is found on the child’s finalPATHwith no shell, thatVENOMX_*is reserved, and that credentials passed with--envare 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(warnwhen the base environment has noPATH, or itsPATHhas empty or relative entries that §6 skips),spawn.base_env(info: counts of base variables stripped by each §4 rule) andspawn.cwd(warnwhen the daemon’s own working directory no longer exists). Add counters ofinvalid_params.envby reason,not_found.cwd,unavailable.cwdby reason andunavailable.spawnby 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:
spawnon the target, unchanged; nofilescheck forcwd(§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
envarray with optionalvaluemeaning unset: fewer flags, but a missingvaluefrom a typo would silently remove a variable. Separateenv_unsetmakes removal explicit. - A string map (
{"FOO":"bar"}): natural JSON, but format 1 has no arbitrary maps, and ordering would be unspecified. - Relative
cwdresolved 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
filesonpath:forcwd: security theatre whilespawncan 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 setsPATHfor the child, and gives two different meanings ofPATHin one request. - A
sensitiveannotation 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
- Inheriting the source pane’s directory on split. Resolved: no
inheritance. The daemon does not track a pane’s live directory
(
/proc/<pid>/cwdreflects only the root process, and OSC 7 is an untrusted observation), so clients passcwdexplicitly. 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.- Clean or configured base environment (an
env_inherit=falseflag, or aspawn.envconfig profile). Deferred to COM-266. Until then,env_unsetcovers specific variables. - 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_STRLENand 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.