Skip to content

Recipes

Practical workflows that combine ixt features. Each recipe shows the commands, the resulting state, and what the ixt.toml looks like if you want to commit it.


🔀 Side-by-side versions of the same tool

Use case. You want to keep ruff at the latest version for everyday work, but also pin ruff==0.5.7 for a legacy project — both reachable from your shell, no juggling.

ixt tool install ruff                                                # exposed as `ruff`
ixt tool install ruff==0.5.7 --slot ruff-old --bare                  # env only, no shim yet
ixt tool config ruff-old expose ruff:ruff-old                        # exposed as `ruff-old`

After this:

$ ixt tool list
  ruff.pypi           0.13.0   [python]  bins: ruff
  ruff-old.ruff.pypi  0.5.7    [python]  bins: ruff-old (-> ruff)

The output shows the two separate names involved:

  • Installed idruff-old.ruff.pypi is the slotted install of the ruff package.
  • After the bin name — the shim ruff-old on PATH points at the ruff binary inside the env.

ixt tool info ruff-old resolves the id prefix and shows the exact id:

$ ixt tool info ruff-old
  ruff
  Id:       ruff-old.ruff.pypi
  Backend:  python
  Package:  ruff
  Spec:     ruff==0.5.7
  ...
  Exposed:
    ruff-old -> /.../envs/ruff-old.ruff.pypi/bin/ruff
Flag / command Role
--slot ruff-old Side-by-side slot. The installed id becomes ruff-old.ruff.pypi; the package being installed is still ruff.
--bare Skip the default __main__ shim. Without it, install would try to expose ruff, collide with the first install, and warn conflict: already exists.
ixt tool config ruff-old expose ruff:ruff-old Renames the exposed shim. Rule orig:alias takes the binary ruff from the env and links it as ruff-old on PATH.

Skipping the rename step

ixt tool install ruff==0.5.7 --slot ruff-old --bare installs the env but leaves it unreachable from PATH (bins: (none) in ixt tool list). Run ixt tool config ruff-old expose ruff:ruff-old to fix it.

Persisting the layout in ixt.toml

Add --save on the install and the toml round-trips through ixt tool export / ixt tool apply. Config steps (expose, inject, …) don't touch ixt.toml — capture them with ixt tool export > ixt.toml once you're happy with the layout:

ixt tool install ruff --save
ixt tool install ruff==0.5.7 --slot ruff-old --bare --save
ixt tool config ruff-old expose ruff:ruff-old
ixt tool export > ixt.toml    # snapshot current state
# ixt.toml
[tools]
"@pypi:ruff" = {}
ruff-old = { install = "@pypi:ruff", version = "==0.5.7", expose = ["ruff:ruff-old"] }

The install = "@pypi:ruff" field tells apply which package to install for the ruff-old slot.


🛡️ Run an unfamiliar npm CLI with runtime policy

Use case. You found a new npm CLI you want to try. The package is recent, from an author you don't know. After installation, you want to run it once without giving the runtime process your tokens or normal home-directory view.

Runtime policy starts after install

The commands below restrict the installed CLI when it is launched through the ixt shim. They do not sandbox bun add, npm lifecycle scripts, or other install-time code. If you do not trust the package installation itself, install it in a disposable VM/container/user first.

ixt tool install @acme/shiny-new-cli

# Nothing passes except basic OS vars
ixt tool config shiny-new-cli env base none
ixt tool config shiny-new-cli env allow 'HOME' 'PATH' 'TERM' 'LANG' 'NO_COLOR'

# Filesystem at runtime: read-only host, selected secret paths hidden
ixt tool config shiny-new-cli fs base app-common
ixt tool config shiny-new-cli fs scratch ~/.aws ~/.ssh ~/.npmrc ~/.config/gh

Dry-run before trusting it:

IXT_SHIM_DEBUG=1 shiny-new-cli --version
[ixt shim: shiny-new-cli] env passed (5 vars): HOME LANG PATH TERM NO_COLOR
[ixt shim: shiny-new-cli] env blocked (41 vars): AWS_ACCESS_KEY_ID GH_TOKEN NPM_TOKEN …
[ixt shim: shiny-new-cli] fs base='app-common' scratch=['~/.aws', '~/.ssh', '~/.npmrc', '~/.config/gh']

Once you're satisfied the runtime behaviour is acceptable, lift the policy and switch to a direct symlink (zero overhead):

ixt tool config shiny-new-cli env base all
ixt tool config shiny-new-cli fs base all

Commit the runtime policy posture in ixt.toml

If you want ixt tool apply to reinstate the policy on a new machine, declare it explicitly:

[tools."@acme/shiny-new-cli"]
env_base   = "none"
env_allow  = ["HOME", "PATH", "TERM", "LANG", "NO_COLOR"]
fs_base    = "app-common"
fs_scratch = ["~/.aws", "~/.ssh", "~/.npmrc", "~/.config/gh"]

🔒 Lock a formatter to its working directory

Use case. You run a code formatter (e.g. biome, prettier) on every save. It has no reason to read ~/.ssh or write outside the project. Lock it down so it can't — even if the package is later compromised.

ixt tool install @biomejs/biome

# Standard linter preset: OS vars + tool-specific vars
ixt tool config biome env base os-common
ixt tool config biome env allow '*BIOME*'

# Filesystem: read the project (already covered by app-common cwd bind),
# hide secrets
ixt tool config biome fs base app-common
ixt tool config biome fs scratch ~/.ssh ~/.aws

app-common already binds the entire host read-only and makes cwd read-write — a formatter needs nothing more. The scratch entries replace ~/.ssh and ~/.aws with empty writable tmpfs: the formatter won't crash (no ENOENT), it just sees nothing there.

ixt.toml for team-wide enforcement

[tools."@biomejs/biome"]
version    = "1.9.4"
env_base   = "os-common"
env_allow  = ["*BIOME*"]
fs_base    = "app-common"
fs_scratch = ["~/.ssh", "~/.aws"]

Commit this and every teammate who runs ixt tool apply gets the same runtime policy enforced on Linux with bwrap — no per-person setup.