Denylist
The denylist is a fast, uniform way to disable runnables installed in a
Python virtual environment — a single one, a set, or all of them — scoped by
username and/or hostname, and combinable. It is a security control: a kill
switch you can flip without uninstalling anything or editing each runnable's
runspec.toml.
Because every runnable depends on runspec and funnels through
runspec.parse(), the denylist is enforced in one place and applies uniformly
to every invocation path: a direct CLI run, an installed entry point, and an
agent call via runspec serve. A disabled runnable returns the same refusal
everywhere.
How it works
On every parse(), runspec reads runspec_denylist.toml from the root of the
virtual environment it is installed in ({sys.prefix} — the same location as
.runspec_env). If a rule matches the runnable being run and the current
effective user and the hostname, the run is refused:
✗ Refusing to run 'deploy': disabled by the runspec denylist.
Reason: change freeze
Denylist: /opt/venv/runspec_denylist.toml
An administrator can re-enable it with `runspec deny clear` (or by editing that file).
When no file is present (the default), nothing is disabled.
Managing it with runspec deny
runspec deny edits the denylist for the venv it runs in. It is part of the
core runspec binary — not a runnable — so it can never disable itself: the
re-enable path always works, even on a host where everything else is disabled.
runspec deny add --runnable deploy # disable one runnable (all users/hosts)
runspec deny add -r deploy -u svc-batch -H 'prod-*' # disable deploy for svc-batch on prod hosts
runspec deny all --reason "change freeze" # disable every runnable in this venv
runspec deny list # show the current mode + rules
runspec deny mode warn # warn instead of refusing (enforce|warn|off)
runspec deny clear # remove the denylist — re-enable everything
--runnable, --user, and --host accept shell-style globs (*) and may be
repeated. An omitted dimension means "all". runspec deny list --json emits the
denylist as structured JSON ({path, present, mode, disabled, reason, rules}) —
what the console's status table reads.
File format
The file is a separate format from runspec.toml:
[denylist]
mode = "enforce" # "enforce" (default) | "warn" | "off"
# disabled = true # fast path: deny everything
[[denylist.rule]]
runnables = ["deploy"] # omitted ⇒ ["*"] (all runnables)
users = ["svc-batch"] # omitted ⇒ ["*"] (all users)
hosts = ["prod-*"] # omitted ⇒ ["*"] (all hosts)
reason = "change freeze" # surfaced in the refusal message
A run is denied when any rule matches. Multiple rules OR together; within a rule, the runnable name, username, and hostname dimensions AND together.
modeis authoritative and lives in this file — there is no per-runnable opt-out, so a runnable author cannot exempt themselves from an operator's denylist.warnlogs to stderr and continues;offdisables enforcement without deleting the rules.- The effective user (post-
sudo/run_asidentity the runnable actually runs as) is matched, alongsidesocket.gethostname(). --helpstill works for a disabled runnable, and introspection (runspec local,runspec stubs) is never blocked.
Because the file carries hostname rules, one canonical denylist can be
distributed across many hosts and still target only the right ones (e.g. disable
deploy on prod-* while leaving dev-* alone).
Why it cannot lock you out
The runspec binary itself is exempt from the denylist, so runspec deny clear,
runspec serve, and runspec local keep working even under a deny-everything
rule. To re-enable on a remote host, run runspec deny clear over SSH (or delete
{sys.prefix}/runspec_denylist.toml).
From the desktop console
The desktop console drives the same runspec deny across your
fleet over the SSH connections it already holds — one call per venv on each host
— in two ways:
- Settings → Denylist — a tab with a host picker (empty ⇒ all hosts),
Disable all / Re-enable all (clear) buttons (danger-confirmed), an
add specific rule form (runnable / user / host globs + reason), and an
enforcement-mode toggle (
enforce/warn/off). The aggregated result is shown inline. -
The agent tool
manage_denylist— ask the agent something like "disable all runnables on prod-* until further notice" or "re-enable everything on prod-2" and approve the confirm prompt. -
targetsselects which fleet hosts to act on; omit it to hit all hosts. addtakes the samerunnable/user/hostglobs andreasonas the CLI;modetakesenforce|warn|off;clearre-enables everything.- The tool is confirm-gated (like file transfers), so a fleet-wide disable is one approval away — and one approval back.
Because the tool shells out to the core runspec binary (which is exempt from
its own denylist), it disables the runnables on those hosts, never the console
itself, and clear can always re-enable them. Remote hosts need
runspec >= 0.43.0 for the tool to take effect.
Testing
Point the denylist at an arbitrary path with the RUNSPEC_DENYLIST_FILE
environment variable (mirrors RUNSPEC_ENV_FILE):
RUNSPEC_DENYLIST_FILE=/tmp/deny.toml runspec deny all
RUNSPEC_DENYLIST_FILE=/tmp/deny.toml deploy # refused
Next: Console quickstart — the runspec-console desktop app.