Skip to content

Program Arguments

Every other block in a fragment decides what a program may reach. This one changes what it does, so it is deliberately the narrowest construct in the language: one verb, and no way to remove, replace or reorder anything you typed.

fragment "codex-app" {
args {
append ["--ignore-certificate-errors-spki-list=$WARDCELL_PROXY_CA_SPKI"]
}
}

One class of program cannot be configured any other way. Chromium — and so every Electron application — reads no certificate authority from its environment, and gives each child process a Seatbelt profile of its own. Under wardcell that means it needs --no-sandbox to start at all, and --ignore-certificate-errors-spki-list before its traffic can be inspected. Both are command-line switches with no environment equivalent.

Nothing else needs it. The JVM takes $WARDCELL_PROXY_CA_TRUSTSTORE through env, cargo takes CARGO_HTTP_CAINFO, and every CLI in the bundled tree is configured the same way. If a variable will do, use one.

Added, never removed. wardcell run -- Codex.app --foo runs with --foo intact, in the position you put it. A policy cannot remove, replace or reorder what you typed.

In front of your own arguments. A fragment is selected on the program, so the switch it adds belongs to the program. wardcell run -- codex login status runs codex --sandbox=… login status, not codex login status --sandbox=… — the second is refused, because that option belongs to codex and not to login status. The front is also the only position that cannot land between one of your options and its value, since a policy has no way to know which of your switches take one.

Never the program itself. A policy decides what a program may reach, not which program runs.

Your value wins. An argument whose switch is already present is dropped, so --user-data-dir=/mine is not joined by a second one from policy. This is the only place a policy reads your command line, and it reads it to stay out of the way. A positional argument is never compared — a file you happen to call no-sandbox does not suppress a switch.

Including a switch you spelled differently, where the fragment says so. A program may accept one option under two names, which no comparison of switch names can see: to codex, -s and --sandbox are one option, and being handed both is an error that stops it starting. unless_given names the other spellings, and the whole block is skipped when you used any of them:

args {
unless_given ["-s"] # the same option, spelled short
append ["--sandbox=danger-full-access"] # what to set when you did not
}

It governs the block, not one entry — so a fragment adding several unrelated switches gives each its own args block.

A short entry matches its value attached as well as separate: -s read-only, -s=read-only and -sread-only are one option to the program and three unrelated strings to anything comparing switch names. You do not need to name the long spelling — an appended --sandbox=… already carries that name, so the first-one-wins rule drops it on its own.

Every run says what was added, on stderr, naming the fragment:

wardcell: policy added argument --no-sandbox (shared/electron-app)
wardcell: policy added argument --ignore-certificate-errors-spki-list=xU+2VP… (apps/codex-app)

This is not optional and it is the reason the block is safe to have. Every other rule in a policy restricts a run, so a quiet run is a safe one; --no-sandbox makes the program weaker, and a quiet run would hide that.

A workspace policy may not use it. A .wardcell file sits in a directory the run can usually write, so a sandbox could otherwise edit the policy its own next run is built from. Filesystem grants from layer 3 are checked against base.sbx and can only widen reach; an appended --gpu-launcher=… would run a program of the sandbox’s choosing. wardcell rejects it.

Only switches. append takes arguments beginning with -. A policy configures how a program runs; what it operates on is yours to say, and a bare path added to your command line would change that.

Fragments union, first one wins. Several fragments may each append, in fragment order, and a switch already appended is not appended again — so two fragments asking for --no-sandbox produce one. Two fragments asking for the same switch with different values also produce one, the earlier, with no error. Keep a switch in a single fragment.

Arguments expand the same tokens an env value does, which is what makes per-run values possible:

Token
$WARDCELL_PROXY_CA_SPKIbase64(SHA-256(SubjectPublicKeyInfo)) of this run’s CA — what Chromium’s --ignore-certificate-errors-spki-list takes
$WARDCELL_PROXY_CA_CERT / _BUNDLE / _TRUSTSTOREthe same CA as a file, in the three forms a runtime might read
$WARDCELL_PLUGIN_DIRthe run’s shared plugin directory, read-only to the sandbox — where a plugin materializes trust and other files
$APP_BUNDLE, $HOME, $WORKDIR, $TMPDIR, $WARDCELL_ZONE, $WARDCELL_WORKDIR_IDas everywhere else

$WARDCELL_PROXY_CA_SPKI changes every run, because the CA does. That is the point: no user could type it, which is why the switch it feeds was unusable before this block existed.

Getting interception back for a Chromium application

Section titled “Getting interception back for a Chromium application”

--ignore-certificate-errors-spki-list is not --ignore-certificate-errors. It does not switch validation off; it trusts exactly one key — the one wardcell generated for this run. With it, allow_http and allow_requests work normally and every request is visible and gated:

fragment "vscode-app" {
args {
append ["--ignore-certificate-errors-spki-list=$WARDCELL_PROXY_CA_SPKI"]
}
net {
allow_http "marketplace.visualstudio.com" { allow_requests ["GET /**"] }
}
}