Environment variable policy¶
Control which environment variables reach a tool — with two distinct protection levels depending on whether bubblewrap is installed.
By default a tool invoked through ixt inherits your full environment: AWS_SECRET_KEY, GH_TOKEN, every API key in your shell. That is fine for tools you trust. For tools installed from an unfamiliar repo, a random npm package, or a cloned project, runtime env policy lets you decide which variables the command receives when you run it through the ixt shim.
ixt tool config <target> env lets you lock that down per tool, using three orthogonal layers.
Runtime only — not install-time
Env policy applies when the installed command is launched through its ixt shim. It does not sandbox package installation (uv pip install, bun add, npm lifecycle scripts such as postinstall) and it does not sandbox ixt lifecycle hooks. If install-time code is untrusted, install in a disposable environment.
Two protection levels — read before relying on this
With bubblewrap installed (effective protection) — the shim
runs the tool inside a bwrap sandbox with --unshare-pid --as-pid-1 and a private /proc. The filtered env reaches the
tool, and /proc/$PPID/environ, /proc/1/environ, and the
namespace-wide /proc/[0-9]*/environ scan all return the
filtered env (or no data) — not the host secrets.
Without bubblewrap (hygiene only) — the shim falls back to
execve(tool, filtered_env) direct. The filtered env reaches the
tool, but a malicious tool can still read /proc/$PPID/environ to
get the unfiltered host env from the shell that launched it,
or read secret files on disk (~/.aws, ~/.netrc, ~/.ssh, …).
Useful for cleaning logs/traces/naive telemetry; not a real
exfiltration barrier.
The shim emits a one-shot warning per shell session in degraded
mode. Silence with IXT_POLICY_QUIET=1. The CLI prints the same
warning unconditionally when you call ixt tool config <target> env …
and bwrap is missing — so you find out at config time, not later.
How it works¶
ixt normally exposes a tool as a symlink in $IXT_HOME/installed/bin/. When you set an env policy, ixt replaces that symlink with a small Python wrapper that:
- Filters
os.environaccording to your policy - If
bwrapis on PATH — runs the tool inside a bubblewrap sandbox with PID isolation, a private/proc, and the host filesystem mounted read-write (--bind / /). Uses--clearenv --setenv K Vto hand only the filtered env to the tool./proc/*/environreads from inside the sandbox cannot reach host secrets. - Otherwise — calls
os.execve(tool, filtered_env)directly. Filtered env reaches the tool;/procreads remain a leak surface (degraded / hygiene mode).
No daemon. The wrapper is a plain text file you can read and audit at any time; effective isolation is delegated to bubblewrap when available.
When you reset the policy (ixt tool config <target> env reset, or env base all with no allow or deny rules), ixt restores the direct symlink. Zero overhead.
The three layers¶
Policy resolution order — later layers narrow down the result:
| Layer | Subcommand | Effect |
|---|---|---|
base |
env base VALUE |
Starting set: everything (all), nothing (none), or a named preset (os-common) |
| allow | env allow PATTERN |
Add env vars matching a glob on top of the base |
| deny | env deny PATTERN |
Remove env vars matching a glob — always wins, regardless of base or allow |
Effective env = (base ∪ allow) ∖ deny
Quick start¶
A typical workflow: start from a restrictive base, add what the tool needs, block anything sensitive.
# 1. Switch to a minimal base — only standard OS vars pass through
ixt tool config ruff env base os-common
# 2. Add the patterns the tool legitimately needs
ixt tool config ruff env allow '*RUFF*'
# 3. Hard-block a specific var even if a glob would match it
ixt tool config ruff env deny RUFF_SECRET_TOKEN
Check the result:
env policy for ruff:
base : os-common (HOME PATH XDG_* NO_COLOR FORCE_COLOR TERM COLORTERM LANG LC_* USER LOGNAME TMPDIR SHELL TZ)
allow: *RUFF*
deny : RUFF_SECRET_TOKEN
env base — the starting set¶
Sets the initial pool of environment variables before allow/deny filtering.
| Value | Starting pool | Use case |
|---|---|---|
all |
Your full os.environ (default) |
Trusted tool — current behaviour, no wrapper |
none |
Empty — nothing passes unless explicitly allowed | Unfamiliar tool, minimum runtime environment |
os-common |
Standard OS and terminal variables (see below) | Most formatters, linters, CLI tools |
os-common expands to:
Start with os-common
os-common covers what ~90 % of formatters and linters actually need. Start there and add specific patterns with env allow if the tool complains.
env allow — the allow list¶
Adds glob patterns on top of the base. Patterns are evaluated at runtime against the current environment, so vars that don't exist yet (set later in your shell) are handled correctly.
ixt tool config ruff env allow '*RUFF*' # any var containing RUFF
ixt tool config ruff env allow 'GIT_*' 'GH_TOKEN' # multiple patterns at once
ixt tool config ruff env allow 'ANTHROPIC_API_KEY' # exact name — no glob needed
Patterns follow Python's fnmatch rules: * matches any sequence, ? matches one character, [seq] matches any character in seq.
Multiple calls accumulate — each call appends to the existing allow list.
env deny — the deny list¶
Adds globs to the deny list. Deny always wins, regardless of base or allow.
ixt tool config ruff env deny 'RUFF_SECRET_TOKEN' # exact name
ixt tool config ruff env deny '*SECRET*' # anything with SECRET
ixt tool config ruff env deny '*TOKEN*' '*KEY*' # multiple in one call
ixt tool config ruff env deny '*TOKEN*' --except GH_TOKEN # keep one allowed token
Deny wins unconditionally
If a var matches both an allow pattern and a deny pattern, it is blocked. This is intentional — deny is your last line of defence.
Use --except when a broad deny pattern is right but one variable still has to
pass. Exceptions are attached to that deny pattern and are persisted in
ixt.json / ixt.toml.
Viewing the current policy¶
Output when a policy is active:
env policy for ruff:
base : os-common (HOME PATH XDG_* NO_COLOR FORCE_COLOR TERM COLORTERM LANG LC_* USER LOGNAME TMPDIR SHELL TZ)
allow: *RUFF* GH_TOKEN
deny : RUFF_SECRET_TOKEN
Output when no policy is set (default):
env policy for ruff:
base : all
allow: (none)
deny : (none)
no policy — tool runs with full environment (symlink)
Degraded mode and opt-outs¶
The shim reads two environment variables before any policy filtering so they remain usable even with the strictest env_base="none" configuration:
| Variable | Effect |
|---|---|
IXT_POLICY_QUIET=1 |
Silence the one-shot per-session warning printed in degraded mode. The protection level is unchanged — only the warning is hidden. |
IXT_DISABLE_BWRAP=1 |
Force degraded mode for env-only policies even when bwrap is installed. Useful for debugging, or for tools that misbehave inside a PID namespace. fs policy still requires bwrap and errors out if combined with this var. |
The runtime warning uses a marker file at $XDG_STATE_HOME/ixt/policy-warned-<sid> (where <sid> = os.getsid(0)) so it appears at most once per shell session. Delete the file to re-trigger it.
Debugging a policy — IXT_SHIM_DEBUG¶
Set IXT_SHIM_DEBUG=1 (or IXT_SHIM_VERBOSE=1) to make the wrapper print what it is passing and blocking before exec:
[ixt shim: ruff] base='os-common' allow=['*RUFF*'] deny=['RUFF_SECRET_TOKEN']
[ixt shim: ruff] env passed (47 vars): COLORTERM HOME LANG LC_ALL PATH RUFF_CACHE_DIR TERM XDG_CONFIG_HOME …
[ixt shim: ruff] env blocked (12 vars): AWS_ACCESS_KEY_ID AWS_SECRET_KEY GH_TOKEN RUFF_SECRET_TOKEN …
Output goes to stderr so it does not interfere with stdout pipelines.
Persisting policy in ixt.toml¶
Declare the policy next to the tool entry so ixt tool apply replays it on a new machine:
[tools.ruff]
env_base = "os-common"
env_allow = ["*RUFF*"]
[tools.ruff.env_deny]
"RUFF_SECRET_TOKEN" = {}
"*TOKEN*" = { except = ["GH_TOKEN"] }
Reference¶
| Action | Arguments | Description |
|---|---|---|
list (default) |
— | Show the current policy |
base |
all\|none\|os-common |
Set the base policy (removes wrapper when reset to all with no other rules) |
allow |
PATTERN ... |
Add one or more glob patterns to the allow list |
deny |
PATTERN ... [--except PATTERN ...] |
Add one or more glob patterns to the deny list (always wins over allow) |
reset |
— | Clear all env policy rules and restore direct execution when no fs policy remains |
Limitations¶
- Linux only (for now). The Python wrapper uses
os.execve, which is unavailable on Windows. On Windows, ixt keeps the direct copy (no symlink, no wrapper);ixt tool config envsaves the policy toixt.jsonbut does not enforce it at runtime. - Hygiene without bubblewrap, effective with it. Without bwrap, the filtered env reaches the tool but
/proc/$PPID/environand friends remain readable — the tool can recover the host env from its parent shell. Install bwrap for an actual exfiltration barrier (PID namespace + private/proc). The shim warns once per session in degraded mode. - Filesystem isolation is a separate feature. This page covers environment variables only. See Filesystem policy for
ixt tool config <target> fs— both policies compose inside the same shim. - No network isolation. Env policy does not block outbound connections. A tool can still send out any data it can read.
- No inheritance from parent tools. Each tool's env policy is independent; injected packages share the same policy as the parent tool.