This file is auto-generated by can-docgen. Do not edit manually.
Canister ships with the recipe files below, grouped by category. Recipes are looked up by name (recursively) across the recipe search path: ./.canister/, $XDG_CONFIG_HOME/canister/recipes/, then /etc/canister/recipes/. The base.toml and default.toml infrastructure recipes are embedded in the binary.
| Recipe | Description |
helm | helm: config + cache + data dirs; repository credentials via env |
kubectl | kubectl: config + cache; token files via KUBECONFIG env only |
podman-config | podman: CLI config + containers dir; registry auth denied |
| Recipe | Description |
example | Example recipe showing all available options |
| Recipe | Description |
lazy-nvim | lazy.nvim plugin manager (clones plugins via git; compose with neovim, git, github) |
neovim | Neovim editor: config + plugin dirs (no network — compose with github etc.) |
opencode | OpenCode AI coding agent: scoped filesystem (no network — compose with LLM service recipes) |
| Recipe | Description |
elixir | Elixir/Erlang runtime: BEAM syscalls + env (no network — compose with hex) |
go | Go toolchain: GOPATH + module cache + config (no proxy — compose with go-proxy) |
node | Node.js runtime (no registry access; compose with npm/pnpm/yarn for installs) |
python | Python 3 interpreter (no PyPI access; compose with pip for installs) |
| Recipe | Description |
asdf | asdf version manager (~/.asdf installs + shims) |
cargo | Rust/Cargo toolchain: registry cache + config; credentials.toml denied (use CARGO_REGISTRIES_* env) |
flatpak | Flatpak applications |
gnu-store | GNU Guix package manager (/gnu/store) |
homebrew | Homebrew/Linuxbrew package manager |
mise | mise polyglot version manager (~/.local/share/mise + config) |
nix-home-manager | Nix home-manager profile (~/.nix-profile + per-user state) |
nix | Nix package manager (/nix/store) |
npm | npm: cache + per-project config; ~/.npmrc denied (use NPM_TOKEN env) |
pip | pip: cache + config; ~/.pypirc denied (use PIP_INDEX_URL env for private indexes) |
pnpm | pnpm: global store + config; auth tokens denied (use NPM_TOKEN env) |
poetry | poetry: config + cache; auth.toml denied (use POETRY_HTTP_BASIC_* env) |
rustup | rustup: toolchain + update config; read-only except for toolchain installs |
snap | Snap package manager (/snap) |
uv | uv: cache + config; auth tokens via env |
yarn | yarn: cache + config; auth tokens denied (use YARN_NPM_AUTH_TOKEN env) |
| Recipe | Description |
anthropic | Anthropic API contracts |
aws | AWS regional endpoint contracts |
crates-io | crates.io Rust crate registry |
github | GitHub REST + GraphQL API contracts |
go-proxy | Go module proxy + checksum database |
gpg-keyservers | Public PGP keyservers (openpgp.org + Ubuntu) |
hex | Hex.pm Elixir/Erlang package registry |
huggingface | Hugging Face Hub contracts |
luarocks | LuaRocks Lua package registry |
npm-registry | npm registry contracts |
openai | OpenAI API contracts |
opencode-ai | OpenCode SaaS + GitHub Copilot endpoints |
pypi | PyPI + pythonhosted contracts |
rust-lang | static.rust-lang.org rustup toolchain distribution |
slack | Slack API contracts |
stripe | Stripe API contracts |
yarn-registry | Yarn classic registry mirror (npm-compatible) |
| Recipe | Description |
generic-strict | Strict no-network policy for untrusted binaries (CI/production) |
gpg-public | gpg: public keyring + trustdb only; private keys denied (compose with gpg-keyservers) |
ssh-agent | agent-mediated SSH via SSH_AUTH_SOCK; no on-disk private keys |
| Recipe | Description |
gh | gh: editor/pager/host config; hosts.yml token denied (use env vars) |
git | git: read user identity + aliases from ~/.gitconfig; credential helpers blocked |
[recipe]
name = "helm"
description = "helm: config + cache + data dirs; repository credentials via env"
version = "1"
[filesystem]
read = [
"$HOME/.config/helm",
]
write = [
"$HOME/.cache/helm",
"$HOME/.local/share/helm",
]
[process]
env_passthrough = [
"HELM_HOME",
"HELM_CACHE_HOME",
"HELM_CONFIG_HOME",
"HELM_DATA_HOME",
"HELM_DRIVER",
"HELM_NAMESPACE",
"KUBECONFIG",
]
[recipe]
name = "kubectl"
description = "kubectl: config + cache; token files via KUBECONFIG env only"
version = "1"
[filesystem]
read = [
"$HOME/.kube/config",
"$HOME/.kube/cache",
]
deny = [
"$HOME/.kube/credentials",
"$HOME/.kube/*.key",
"$HOME/.kube/*.pem",
]
[process]
env_passthrough = [
"KUBECONFIG",
"KUBERNETES_SERVICE_HOST",
"KUBERNETES_SERVICE_PORT",
"KUBECTL_EXTERNAL_DIFF",
]
[recipe]
name = "podman-config"
description = "podman: CLI config + containers dir; registry auth denied"
version = "1"
[filesystem]
read = [
"$HOME/.config/containers/containers.conf",
"$HOME/.config/containers/registries.conf",
"$HOME/.config/containers/policy.json",
"$HOME/.config/containers/storage.conf",
]
deny = [
"$HOME/.config/containers/auth.json",
]
[process]
env_passthrough = [
"CONTAINERS_CONF",
"CONTAINERS_REGISTRIES_CONF",
"CONTAINER_HOST",
"REGISTRY_AUTH_FILE",
]
# Example Canister recipe — a complete sandbox policy for Python scripts.
#
# System paths (/usr/lib, /usr/bin, /lib, /tmp, etc.) are provided by
# base.toml — recipes only need to add application-specific paths.
#
# Usage: can run -r example -- python3 script.py
[recipe]
name = "example"
description = "Example recipe showing all available options"
version = "2"
# Strict mode: abort if any isolation layer cannot be set up.
# Recommended for CI. Uncomment to enable.
# strict = true
[filesystem]
# Paths the sandboxed process can read (mounted read-only).
# System paths are already provided by base.toml — only add app-specific paths here.
read = []
# Paths the sandboxed process can write to (changes persist on host).
# The working directory ($PWD) is always writable. Uncomment for additional paths:
# write = ["/var/data/myapp"]
# Paths explicitly denied (checked before allow and allow_write).
deny = ["/etc/shadow", "/root"]
[network]
# egress: "proxy" (default — inspected via the local proxy) or "none".
# Unfiltered/direct egress lives in [unsafe] (see bottom).
egress = "proxy"
# Allowed upstream destinations. The proxy refuses any other host.
[[host]]
domain = "pypi.org"
[[host]]
domain = "files.pythonhosted.org"
[process]
# Max child PIDs.
max_pids = 64
# Which binaries may exec: "any" (default), "entrypoint-only", or an
# explicit allow list. Uncomment to lock down:
# exec = ["/usr/bin/python3"]
# Environment variables passed through from host.
env_passthrough = ["PATH", "HOME", "LANG", "TERM"]
# [resources] section — cgroup v2 resource limits (optional).
# Requires cgroups v2 on the host. Uncomment to enable:
# [resources]
# memory_mb = 512
# cpu_percent = 50
# [syscalls] section — customize the default seccomp baseline.
# Ordinary (non-dangerous) syscalls only; default-deny allow-list mode.
# [syscalls]
# allow_extra = ["statx"] # add a benign syscall to the allow list
# deny_extra = ["personality"] # remove personality from allow, add to deny
# [unsafe] section — settings that LOWER isolation. A recipe with no
# [unsafe] block provably cannot weaken the baseline. Uncomment knobs as
# needed (each is greppable and surfaced at runtime):
# [unsafe]
# unfiltered_egress = true # direct egress — DLP & contracts OFF
# reachable_ips = ["10.0.0.5/32"] # un-scanned IP-literal egress
# host_loopback = true # reach host 127.0.0.1 services
# expose_ports = ["8080:80"] # forward host ports into the sandbox
# seccomp_default_allow = true # flip seccomp to default-ALLOW
# extra_syscalls = ["ptrace"] # high-risk syscalls (ptrace, bpf, …)
# Canister recipe — lazy.nvim plugin manager for Neovim.
#
# lazy.nvim clones plugins from GitHub (and occasionally luarocks)
# into $XDG_DATA_HOME/nvim/lazy/ on first launch and on `:Lazy sync`.
# The clone target lives under ~/.local/share/nvim/ which the
# `neovim` recipe already mounts; this recipe exists to surface the
# network and tooling dependencies the plugin manager itself needs:
#
# recipes = ["neovim", "lazy-nvim", "git", "github"]
#
# and set `[sandbox.X.network] egress = "proxy"` in the manifest.
#
# Add `luarocks` to the recipe list if any of your plugins declare
# rocks dependencies (e.g. via `:Lazy build`).
[recipe]
name = "lazy-nvim"
description = "lazy.nvim plugin manager (clones plugins via git; compose with `neovim`, `git`, `github`)"
version = "1"
suggests = ["neovim", "git", "github", "luarocks"]
[filesystem]
# Lazy keeps its own log + state under nvim's data dir (already mounted
# by the neovim recipe). The lockfile lives in the project working dir.
# Nothing additional to mount here; this recipe is primarily a discovery
# hint for the builder's suggests graph.
read = []
[process]
env_passthrough = [
# Lazy honours these for offline / single-plugin modes.
"LAZY_PATH",
"LAZY_STDPATH",
]
# Canister recipe for Neovim with LSP support.
#
# Mounts the user's full Neovim configuration (config, data, state, cache)
# so plugin managers (lazy.nvim), LSP servers (via Mason), tree-sitter
# parsers, and other tooling work out of the box.
#
# System paths (/usr/lib, /usr/bin, /lib, /tmp, etc.) are provided by
# base.toml — this recipe only adds Neovim-specific paths. Network
# access for plugin / LSP / Mason updates is NOT baked in. Compose with
# the service recipes for whichever ecosystems you actually use:
#
# recipes = ["neovim", "github"] # plugins from GitHub
# recipes = ["neovim", "github", "luarocks"] # plus rocks
# recipes = ["neovim", "github", "npm-registry", "pypi"] # plus Mason LSPs
#
# and set `[sandbox.X.network] egress = "proxy"` in the manifest.
[recipe]
name = "neovim"
description = "Neovim editor: config + plugin dirs (no network — compose with `github` etc.)"
version = "3"
suggests = ["lazy-nvim", "git", "github", "luarocks"]
[filesystem]
read = [
# Neovim XDG directories
"$HOME/.config/nvim",
"$HOME/.local/share/nvim",
"$HOME/.local/state/nvim",
"$HOME/.cache/nvim",
]
[process]
max_pids = 256
env_passthrough = [
"PATH",
"HOME",
"LANG",
"TERM",
"COLORTERM",
"TERMINFO",
"USER",
"SHELL",
"EDITOR",
"VISUAL",
# XDG directories (so nvim finds its config)
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_STATE_HOME",
"XDG_CACHE_HOME",
"XDG_RUNTIME_DIR",
# Nix
"NIX_PATH",
"NIX_PROFILES",
# LSP / language tooling
"CARGO_HOME",
"RUSTUP_HOME",
"GOPATH",
"GOROOT",
"NODE_PATH",
"npm_config_prefix",
]
[syscalls]
# memfd_create is used by various LSP runtimes.
allow_extra = ["memfd_create"]
# ptrace (LSP/debugger support) is process-inspection/injection capable,
# so it is declared explicitly rather than hidden in allow_extra.
[unsafe]
extra_syscalls = ["ptrace"]
# Canister recipe for OpenCode — AI coding agent.
#
# OpenCode is a Go binary that talks to LLM providers (GitHub Copilot,
# Anthropic, OpenAI, etc.) and runs developer tools (git, cargo, npm,
# ripgrep, etc.) to assist with coding tasks.
#
# This recipe scopes filesystem access to the project working directory
# and OpenCode's own state/config dirs. Network access to LLM APIs is
# NOT baked in — compose with the relevant service recipes:
#
# # GitHub Copilot
# recipes = ["opencode", "github", "opencode-ai"]
#
# # Anthropic
# recipes = ["opencode", "anthropic", "opencode-ai"]
#
# # OpenAI
# recipes = ["opencode", "openai", "opencode-ai"]
#
# and set `[sandbox.X.network] egress = "proxy"` in the manifest.
#
# Note: Run from the project directory you want OpenCode to work on.
# The $PWD will be bind-mounted into the sandbox.
[recipe]
name = "opencode"
description = "OpenCode AI coding agent: scoped filesystem (no network — compose with LLM service recipes)"
version = "3"
suggests = ["git", "github", "anthropic", "openai", "opencode-ai"]
[filesystem]
# OpenCode needs access to:
# - The project directory (implicitly mounted writable as the working dir)
# - Its own config and state dirs
# System paths (/usr/lib, /usr/bin, /lib, /tmp, etc.) are provided by base.toml.
# Language toolchains ($HOME/.cargo, $HOME/.rustup, /nix/store, etc.) should
# be added via recipe composition in canister.toml, NOT baked in here.
#
# SECURITY: We intentionally do NOT mount:
# - $HOME/.ssh — SSH private keys. Use SSH_AUTH_SOCK (agent) instead.
# - $HOME/.gitconfig — may contain credential helpers or tokens. Git identity
# is passed via GIT_AUTHOR_NAME/EMAIL env vars.
# - $HOME/.cargo — contains credentials.toml (crates.io tokens). Mount
# via the cargo recipe when needed.
# - $HOME/.gnupg — GPG private keys.
# - $HOME/.aws — AWS credentials.
# - $HOME/.kube — Kubernetes config with tokens.
read = [
"$HOME/.opencode",
"$HOME/.config/opencode",
]
# OpenCode writes to its state dir (SQLite DB, logs, tool output).
write = [
"$HOME/.local/share/opencode",
]
deny = [
# Universal denies (/etc/shadow, /etc/gshadow, /root) already live in
# base.toml — recipe focuses on credential / token files OpenCode
# has no business reading.
"$HOME/.ssh",
"$HOME/.gnupg",
"$HOME/.aws",
"$HOME/.kube",
"$HOME/.docker",
"$HOME/.npmrc",
"$HOME/.pypirc",
"$HOME/.netrc",
"$HOME/.config/gh",
]
[process]
# OpenCode spawns subprocesses for tools (git, cargo, rg, etc.)
max_pids = 256
# Environment variables OpenCode and its tools need.
env_passthrough = [
"PATH",
"HOME",
"LANG",
"TERM",
"COLORTERM",
"EDITOR",
"SHELL",
"USER",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_STATE_HOME",
"XDG_CACHE_HOME",
"GIT_AUTHOR_NAME",
"GIT_AUTHOR_EMAIL",
"GIT_COMMITTER_NAME",
"GIT_COMMITTER_EMAIL",
"HTTPS_PROXY",
"HTTP_PROXY",
"NO_PROXY",
"SSH_AUTH_SOCK",
"CARGO_HOME",
"RUSTUP_HOME",
"NODE_PATH",
]
# Canister recipe for Elixir/Erlang workloads.
#
# Covers only what the BEAM runtime itself needs: syscalls, env vars,
# and PID budget. Network access (hex.pm, GitHub deps) and egress mode
# are NOT baked in — compose with the appropriate service recipes:
#
# recipes = ["elixir", "hex"] # mix deps.get from Hex
# recipes = ["elixir", "hex", "github"] # plus GitHub-sourced deps
#
# and set `[sandbox.X.network] egress = "proxy"` in the manifest
# to enforce the allow list.
[recipe]
name = "elixir-dev"
description = "Elixir/Erlang runtime: BEAM syscalls + env (no network — compose with `hex`)"
version = "3"
suggests = ["hex", "git", "gh"]
[syscalls]
# BEAM uses memfd_create for JIT code loading (moved to deny list in
# default baseline, allowed back here).
allow_extra = ["memfd_create"]
# BEAM uses ptrace for tracing/debugging tools (:observer, :dbg). ptrace is
# process-inspection/injection capable, so it is declared explicitly.
[unsafe]
extra_syscalls = ["ptrace"]
[process]
# BEAM spawns many lightweight processes via OS threads; the default
# scheduler count equals the CPU core count. 256 is generous for most
# mix tasks and development servers.
max_pids = 256
env_passthrough = [
"PATH",
"HOME",
"LANG",
"TERM",
"MIX_ENV",
"MIX_HOME",
"HEX_HOME",
"ERL_AFLAGS",
"ELIXIR_ERL_OPTIONS",
"RELEASE_COOKIE",
"RELEASE_NODE",
"RELEASE_DISTRIBUTION",
"SECRET_KEY_BASE",
"DATABASE_URL",
"PHX_HOST",
"PHX_SERVER",
"PORT",
]
# Canister recipe for the Go toolchain.
#
# Covers GOPATH / module cache filesystem layout and the env vars
# `go build` / `go run` honour. Network access to the module proxy is
# NOT baked in — compose with `go-proxy` when fetching modules:
#
# recipes = ["go"] # build/test with locally vendored deps
# recipes = ["go", "go-proxy"] # plus `go mod download` from the proxy
[recipe]
name = "go"
description = "Go toolchain: GOPATH + module cache + config (no proxy — compose with `go-proxy`)"
version = "2"
suggests = ["go-proxy", "git", "github"]
[filesystem]
read = [
"$HOME/.config/go/env",
]
write = [
"$HOME/go",
"$HOME/.cache/go-build",
]
[process]
env_passthrough = [
"GOPATH",
"GOROOT",
"GOBIN",
"GOPROXY",
"GONOSUMCHECK",
"GONOSUMDB",
"GOPRIVATE",
"GOFLAGS",
"GOMODCACHE",
"CGO_ENABLED",
]
# Canister recipe — Node.js language runtime.
#
# Covers what's needed to *run* node: env vars the runtime honours
# and read access to module paths already mounted via base.toml.
# Does NOT permit network access or registry fetching — for
# `npm install` / `yarn install` / `pnpm install`, compose with the
# matching package-manager recipe.
#
# recipes = ["node"] # just run node
# recipes = ["node", "npm"] # plus npm install from npmjs.org
[recipe]
name = "node"
description = "Node.js runtime (no registry access; compose with `npm`/`pnpm`/`yarn` for installs)"
version = "1"
suggests = ["npm", "pnpm", "yarn", "npm-registry", "git"]
[process]
env_passthrough = [
"NODE_ENV",
"NODE_OPTIONS",
"NODE_PATH",
"NODE_NO_WARNINGS",
]
# Canister recipe — Python 3 language runtime.
#
# Covers what's needed to *run* python3: env vars the interpreter
# honours, and read access to site-packages already mounted via
# base.toml. Does NOT permit network access or PyPI fetching — for
# `pip install`, compose with the `pip` recipe.
#
# recipes = ["python"] # just run python3 scripts
# recipes = ["python", "pip"] # plus install from PyPI
[recipe]
name = "python"
description = "Python 3 interpreter (no PyPI access; compose with `pip` for installs)"
version = "1"
suggests = ["pip", "uv", "poetry", "pypi", "git"]
[process]
env_passthrough = [
"PYTHONPATH",
"PYTHONDONTWRITEBYTECODE",
"PYTHONUNBUFFERED",
"PYTHONIOENCODING",
"PYTHONHASHSEED",
"VIRTUAL_ENV",
]
# Canister recipe — asdf version manager.
#
# asdf installs language toolchains under ~/.asdf/installs/<plugin>/<version>
# and exposes them via shims in ~/.asdf/shims. The shims dispatch to the
# real binary, so when a sandboxed command resolves through a shim, the
# realpath ends up under ~/.asdf/installs/...
#
# Compose with the language recipe you're actually using:
# recipes = ["asdf", "elixir", "hex"]
# recipes = ["asdf", "node", "npm", "npm-registry"]
[recipe]
name = "asdf"
description = "asdf version manager (~/.asdf installs + shims)"
version = "1"
suggests = []
[filesystem]
read = [
"$HOME/.asdf",
"$HOME/.tool-versions",
"$HOME/.asdfrc",
]
write = [
"$HOME/.asdf/downloads",
"$HOME/.asdf/tmp",
]
[process]
env_passthrough = [
"ASDF_DIR",
"ASDF_DATA_DIR",
"ASDF_CONFIG_FILE",
"ASDF_DEFAULT_TOOL_VERSIONS_FILENAME",
]
# Canister recipe for Cargo (Rust) toolchain.
#
# Cargo installs binaries to $HOME/.cargo/bin and stores the toolchain
# (rustc, rustup) under $HOME/.rustup.
[recipe]
name = "cargo"
description = "Rust/Cargo toolchain: registry cache + config; credentials.toml denied (use CARGO_REGISTRIES_* env)"
version = "3"
suggests = ["rustup", "crates-io"]
# Network access to crates.io is NOT baked in. Compose with `crates-io`
# (and `rust-lang` for rustup distributions) when fetching:
# recipes = ["cargo", "crates-io"]
[filesystem]
read = [
"$HOME/.cargo/config.toml",
"$HOME/.cargo/config",
"$HOME/.rustup",
]
write = [
"$HOME/.cargo/registry",
"$HOME/.cargo/git",
"$HOME/.cargo/advisory-db",
]
deny = [
"$HOME/.cargo/credentials.toml",
"$HOME/.cargo/credentials",
]
[process]
env_passthrough = [
"CARGO_HOME",
"CARGO_TARGET_DIR",
"CARGO_REGISTRIES_CRATES_IO_PROTOCOL",
"CARGO_NET_GIT_FETCH_WITH_CLI",
"RUSTFLAGS",
"RUSTDOCFLAGS",
]
# Canister recipe for Flatpak applications.
#
# Flatpak installs applications under /var/lib/flatpak (system-wide)
# and $HOME/.local/share/flatpak (per-user).
[recipe]
name = "flatpak"
description = "Flatpak applications"
version = "1"
[filesystem]
read = ["/var/lib/flatpak", "$HOME/.local/share/flatpak"]
# Canister recipe for GNU Guix package manager.
#
# Guix uses a content-addressed store at /gnu/store, similar to Nix.
# Binaries reference sibling store entries, so the entire store must
# be mounted.
[recipe]
name = "gnu-store"
description = "GNU Guix package manager (/gnu/store)"
version = "1"
[filesystem]
read = ["/gnu/store"]
# Canister recipe for Homebrew (Linuxbrew) package manager.
#
# On Linux, Homebrew installs to /home/linuxbrew/.linuxbrew or
# /opt/homebrew (rare on Linux, common path on macOS). Binaries
# reference the Cellar and shared libraries within the prefix.
[recipe]
name = "homebrew"
description = "Homebrew/Linuxbrew package manager"
version = "1"
[filesystem]
read = ["/opt/homebrew", "/home/linuxbrew/.linuxbrew"]
# Canister recipe — mise (formerly rtx) polyglot version manager.
#
# mise installs language toolchains under ~/.local/share/mise/installs
# and exposes them via shims in ~/.local/share/mise/shims. Configuration
# lives in ~/.config/mise/config.toml plus per-project .mise.toml files.
#
# Compose with the language recipe you're actually using:
# recipes = ["mise", "node", "npm", "npm-registry"]
# recipes = ["mise", "python", "uv", "pypi"]
[recipe]
name = "mise"
description = "mise polyglot version manager (~/.local/share/mise + config)"
version = "1"
suggests = []
[filesystem]
read = [
"$HOME/.config/mise",
]
write = [
"$HOME/.local/share/mise",
"$HOME/.local/state/mise",
"$HOME/.cache/mise",
]
[process]
env_passthrough = [
"MISE_DATA_DIR",
"MISE_CONFIG_DIR",
"MISE_CACHE_DIR",
"MISE_STATE_DIR",
"MISE_SHELL",
"MISE_ENV",
]
# Canister recipe — Nix home-manager.
#
# home-manager installs user-scoped packages under the per-user profile
# at ~/.nix-profile and stores its own state under ~/.local/state/nix
# and ~/.local/state/home-manager. Configuration lives in
# ~/.config/home-manager (or ~/.config/nixpkgs/home.nix on older setups).
#
# Composes with `nix` (which mounts /nix/store for the binaries the
# profile symlinks into).
#
# recipes = ["nix", "nix-home-manager"]
[recipe]
name = "nix-home-manager"
description = "Nix home-manager profile (~/.nix-profile + per-user state)"
version = "1"
suggests = ["nix"]
[filesystem]
read = [
"$HOME/.nix-profile",
"$HOME/.config/home-manager",
"$HOME/.config/nixpkgs",
]
write = [
"$HOME/.local/state/nix",
"$HOME/.local/state/home-manager",
"$HOME/.cache/nix",
]
[process]
env_passthrough = [
"NIX_PATH",
"NIX_PROFILES",
"NIX_USER_PROFILE_DIR",
"NIX_SSL_CERT_FILE",
"HOME_MANAGER_BACKUP_EXT",
]
# Canister recipe for Nix package manager.
#
# Nix stores all packages in /nix/store with content-addressed paths.
# Binaries freely reference sibling store entries, so the entire store
# must be mounted.
[recipe]
name = "nix"
description = "Nix package manager (/nix/store)"
version = "1"
suggests = ["nix-home-manager"]
[filesystem]
# Mount the entire Nix store — binaries reference sibling entries
# via rpaths and wrapper scripts.
read = ["/nix/store"]
# Canister recipe — npm
#
# Mounts the npm cache and per-user config so `npm install` works
# against the public registry. Does NOT mount ~/.npmrc by default
# because it commonly contains _authToken entries for private
# registries; mounting it would let a malicious dependency read the
# token off disk.
#
# For public-registry workflows this recipe is sufficient. For private
# registries:
# * preferred: pass NPM_TOKEN / NODE_AUTH_TOKEN via env_passthrough,
# and add a `[[host]] domain = "registry.example.com"` block to
# your canister.toml so egress to other hosts is blocked.
# * fallback: add `[filesystem] read = ["$HOME/.npmrc"]` in your
# canister.toml, accepting that any in-sandbox process can read
# the token from disk.
[recipe]
name = "npm"
description = "npm: cache + per-project config; ~/.npmrc denied (use NPM_TOKEN env)"
version = "1"
suggests = ["node", "npm-registry"]
[filesystem]
# Cache and per-user config dir (XDG layout). Cache is writable so
# `npm install` can populate it.
write = [
"$HOME/.npm",
"$HOME/.config/npm",
]
deny = [
"$HOME/.npmrc",
# Older npm versions stored auth alongside the cache.
"$HOME/.npm/_auth",
"$HOME/.npm/_authToken",
]
[process]
env_passthrough = [
"NPM_TOKEN",
"NODE_AUTH_TOKEN",
"NPM_CONFIG_USERCONFIG",
"NPM_CONFIG_CACHE",
"NPM_CONFIG_PREFIX",
"NPM_CONFIG_REGISTRY",
"NODE_PATH",
"NODE_OPTIONS",
]
[recipe]
name = "pip"
description = "pip: cache + config; ~/.pypirc denied (use PIP_INDEX_URL env for private indexes)"
version = "1"
suggests = ["python", "pypi"]
[filesystem]
read = [
"$HOME/.config/pip",
]
write = [
"$HOME/.cache/pip",
]
deny = [
"$HOME/.pypirc",
"$HOME/.pip/pip.conf",
]
[process]
env_passthrough = [
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
"PIP_CACHE_DIR",
"VIRTUAL_ENV",
"PYTHONPATH",
"PYTHONDONTWRITEBYTECODE",
]
[recipe]
name = "pnpm"
description = "pnpm: global store + config; auth tokens denied (use NPM_TOKEN env)"
version = "1"
suggests = ["node", "npm-registry"]
[filesystem]
write = [
"$HOME/.local/share/pnpm",
"$HOME/.local/share/pnpm/store",
"$HOME/.config/pnpm",
]
deny = [
"$HOME/.npmrc",
]
[process]
env_passthrough = [
"NPM_TOKEN",
"NODE_AUTH_TOKEN",
"NPM_CONFIG_REGISTRY",
"PNPM_HOME",
"NODE_PATH",
"NODE_OPTIONS",
]
[recipe]
name = "poetry"
description = "poetry: config + cache; auth.toml denied (use POETRY_HTTP_BASIC_* env)"
version = "1"
suggests = ["python", "pypi"]
[filesystem]
read = [
"$HOME/.config/pypoetry/config.toml",
]
write = [
"$HOME/.cache/pypoetry",
"$HOME/.local/share/pypoetry",
]
deny = [
"$HOME/.config/pypoetry/auth.toml",
"$HOME/.pypirc",
]
[process]
env_passthrough = [
"POETRY_HOME",
"POETRY_CACHE_DIR",
"POETRY_VIRTUALENVS_IN_PROJECT",
"POETRY_VIRTUALENVS_CREATE",
"POETRY_HTTP_BASIC_PYPI_USERNAME",
"POETRY_HTTP_BASIC_PYPI_PASSWORD",
"VIRTUAL_ENV",
"PYTHONPATH",
]
[recipe]
name = "rustup"
description = "rustup: toolchain + update config; read-only except for toolchain installs"
version = "1"
suggests = ["cargo", "rust-lang", "crates-io"]
[filesystem]
read = [
"$HOME/.rustup/settings.toml",
"$HOME/.rustup/toolchains",
]
write = [
"$HOME/.rustup/downloads",
"$HOME/.rustup/tmp",
"$HOME/.rustup/update-hashes",
]
[process]
env_passthrough = [
"RUSTUP_HOME",
"RUSTUP_TOOLCHAIN",
"RUSTUP_DIST_SERVER",
"RUSTUP_UPDATE_ROOT",
]
# Canister recipe for Snap packages.
#
# Snap installs applications under /snap with the runtime under
# /snap/core*. Binaries are launched via /snap/bin wrappers.
[recipe]
name = "snap"
description = "Snap package manager (/snap)"
version = "1"
[filesystem]
read = ["/snap"]
[recipe]
name = "uv"
description = "uv: cache + config; auth tokens via env"
version = "1"
suggests = ["python", "pypi"]
[filesystem]
read = [
"$HOME/.config/uv",
]
write = [
"$HOME/.cache/uv",
"$HOME/.local/share/uv",
]
deny = [
"$HOME/.pypirc",
]
[process]
env_passthrough = [
"UV_INDEX_URL",
"UV_EXTRA_INDEX_URL",
"UV_CACHE_DIR",
"UV_PYTHON",
"UV_PYTHON_PREFERENCE",
"VIRTUAL_ENV",
"PYTHONPATH",
]
[recipe]
name = "yarn"
description = "yarn: cache + config; auth tokens denied (use YARN_NPM_AUTH_TOKEN env)"
version = "1"
suggests = ["node", "yarn-registry"]
[filesystem]
read = [
"$HOME/.yarnrc",
"$HOME/.yarnrc.yml",
]
write = [
"$HOME/.yarn",
"$HOME/.cache/yarn",
]
deny = [
"$HOME/.npmrc",
]
[process]
env_passthrough = [
"YARN_NPM_AUTH_TOKEN",
"NPM_TOKEN",
"NODE_AUTH_TOKEN",
"NPM_CONFIG_REGISTRY",
"NODE_PATH",
"NODE_OPTIONS",
]
# Anthropic API.
[recipe]
name = "anthropic"
description = "Anthropic API contracts"
# Fake-secret swap: the sandbox gets a fake ANTHROPIC_API_KEY; the proxy
# swaps in the real value only on egress to the anthropic_key-scoped host
# below. See docs/refusals.md.
[network.dlp]
fake_secrets = [{ env = "ANTHROPIC_API_KEY", credential = "anthropic_key" }]
[[host]]
domain = "api.anthropic.com"
methods = ["GET", "POST"]
content_types = ["application/json"]
max_request_bytes = 4194304 # 4 MiB
allow_credentials = ["anthropic_key"]
# AWS service endpoints.
#
# AWS uses regional FQDNs (`s3.us-east-1.amazonaws.com`,
# `sts.us-west-2.amazonaws.com`, etc.) so wildcarding under
# `*.amazonaws.com` is the only practical contract — enumerating
# every service × every region isn't tractable.
#
# Content types include the AWS-proprietary `application/x-amz-json-1.1`
# (used by most services) and `application/xml` (S3 / SQS legacy).
[recipe]
name = "aws"
description = "AWS regional endpoint contracts"
[[host]]
domain = "*.amazonaws.com"
methods = ["GET", "POST", "PUT", "DELETE", "HEAD"]
content_types = [
"application/x-amz-json-1.0",
"application/x-amz-json-1.1",
"application/xml",
"application/json",
"application/octet-stream",
"text/xml",
]
max_request_bytes = 5368709120 # 5 GiB — S3 single-PUT cap
allow_credentials = ["aws_access_key"]
# crates.io — Rust crate registry.
#
# `crates.io` serves the web + publish API (cargo publish POSTs to here).
# `index.crates.io` is the sparse HTTP index (JSON deltas) introduced in
# Cargo 1.68. `static.crates.io` is the crate-tarball CDN.
[recipe]
name = "crates-io"
description = "crates.io Rust crate registry"
suggests = ["cargo"]
[[host]]
domain = "crates.io"
methods = ["GET", "HEAD", "POST", "PUT", "DELETE"]
content_types = ["application/json", "application/octet-stream"]
max_request_bytes = 104857600 # 100 MiB
[[host]]
domain = "index.crates.io"
methods = ["GET", "HEAD"]
content_types = ["application/json", "text/plain"]
[[host]]
domain = "static.crates.io"
methods = ["GET", "HEAD"]
contract_mode = "relaxed"
# GitHub REST + GraphQL APIs.
#
# Both `api.github.com` and `*.github.com` are scoped to JSON. Asset
# uploads use the distinct `uploads.github.com` host (it accepts
# octet-stream releases and packages — see github-uploads.toml).
#
# `allow_credentials = ["github_pat"]` downgrades a github_pat
# detector hit on this host from Block to Warn — legitimate API
# clients carry a PAT in `Authorization: Bearer …`.
[recipe]
name = "github"
description = "GitHub REST + GraphQL API contracts"
suggests = ["git", "gh"]
# Fake-secret swap: the sandbox is given a fake GITHUB_TOKEN / GH_TOKEN
# (a pattern-valid `ghp_…`); the proxy swaps in the real host value only
# on egress to a github_pat-scoped host below. The real token never enters
# the sandbox, so an exfil attempt — even an encrypted one — leaks only the
# useless fake. See docs/refusals.md.
[network.dlp]
fake_secrets = [
{ env = "GITHUB_TOKEN", credential = "github_pat" },
{ env = "GH_TOKEN", credential = "github_pat" },
]
[[host]]
domain = "api.github.com"
methods = ["GET", "POST", "PATCH", "PUT", "DELETE", "HEAD"]
content_types = ["application/json", "application/vnd.github+json", "application/vnd.github.v3+json"]
max_request_bytes = 1048576
allow_credentials = ["github_pat"]
[[host]]
domain = "github.com"
methods = ["GET", "HEAD"]
allow_credentials = ["github_pat"]
[[host]]
domain = "*.github.com"
methods = ["GET", "HEAD"]
allow_credentials = ["github_pat"]
[[host]]
domain = "uploads.github.com"
methods = ["POST"]
content_types = ["application/octet-stream"]
max_request_bytes = 1073741824 # 1 GiB for release-asset uploads
allow_credentials = ["github_pat"]
[[host]]
domain = "objects.githubusercontent.com"
methods = ["GET", "HEAD"]
# Content-served-from-storage: blob download endpoint. No credentials
# expected; downgrade to Relaxed because the path/content-type set is
# huge and not worth enumerating.
contract_mode = "relaxed"
[[host]]
domain = "raw.githubusercontent.com"
methods = ["GET", "HEAD"]
# Raw-content endpoint serving arbitrary file types from repositories.
# Content set is too heterogeneous to enumerate.
contract_mode = "relaxed"
[[host]]
domain = "codeload.github.com"
methods = ["GET", "HEAD"]
# Tarball / zip download endpoint used by `git clone`, `gh repo clone`,
# and codeload links. Returns application/octet-stream for archives.
contract_mode = "relaxed"
# Go module proxy + checksum database.
#
# `proxy.golang.org` is the public module proxy: GETs return JSON
# version lists, .info metadata, .mod files, or .zip module archives.
# `sum.golang.org` is the checksum database (Merkle-tree signed
# verification). Together they cover everything `go mod download`
# / `go get` does over HTTPS by default.
[recipe]
name = "go-proxy"
description = "Go module proxy + checksum database"
suggests = ["go"]
[[host]]
domain = "proxy.golang.org"
methods = ["GET", "HEAD"]
# Module archives can be large for vendored deps; content set spans
# json/octet-stream/text per endpoint.
contract_mode = "relaxed"
[[host]]
domain = "sum.golang.org"
methods = ["GET", "HEAD"]
content_types = ["text/plain", "application/json"]
# Public PGP keyservers.
#
# Used by GPG to fetch and refresh public keys (`gpg --recv-keys`,
# `gpg --refresh-keys`). The OpenPGP HKP protocol travels over HTTPS
# with text/plain responses for armored keys and JSON for VKS lookups.
[recipe]
name = "gpg-keyservers"
description = "Public PGP keyservers (openpgp.org + Ubuntu)"
[[host]]
domain = "keys.openpgp.org"
methods = ["GET", "HEAD"]
content_types = ["application/json", "text/plain", "application/pgp-keys"]
[[host]]
domain = "keyserver.ubuntu.com"
methods = ["GET", "HEAD"]
content_types = ["text/plain", "application/pgp-keys", "text/html"]
# Hex.pm — Elixir/Erlang package registry.
#
# `hex.pm` serves the JSON API and `mix hex.publish` POSTs tarballs.
# `repo.hex.pm` is the read-only package store (octet-stream tarballs +
# signature files). `builds.hex.pm` hosts precompiled OTP/Elixir builds.
[recipe]
name = "hex"
description = "Hex.pm Elixir/Erlang package registry"
suggests = ["elixir"]
[[host]]
domain = "hex.pm"
methods = ["GET", "HEAD", "POST"]
content_types = ["application/json", "application/octet-stream"]
max_request_bytes = 104857600 # 100 MiB
[[host]]
domain = "repo.hex.pm"
methods = ["GET", "HEAD"]
contract_mode = "relaxed"
[[host]]
domain = "builds.hex.pm"
methods = ["GET", "HEAD"]
contract_mode = "relaxed"
# Hugging Face Hub.
#
# Heterogeneous: JSON for the API, multipart/form-data + LFS
# uploads for model push, octet-stream for model downloads. Path
# allow-list keeps scans focused on the API surface; downloads use
# a relaxed sibling block to avoid enumerating every model.
[recipe]
name = "huggingface"
description = "Hugging Face Hub contracts"
[[host]]
domain = "huggingface.co"
methods = ["GET", "POST", "PUT", "DELETE", "HEAD"]
content_types = [
"application/json",
"multipart/form-data",
"application/x-www-form-urlencoded",
"application/octet-stream",
]
max_request_bytes = 5368709120 # 5 GiB; big-model pushes
[[host]]
domain = "*.huggingface.co"
methods = ["GET", "HEAD"]
contract_mode = "relaxed"
[[host]]
domain = "cdn-lfs.huggingface.co"
methods = ["GET", "PUT", "HEAD"]
contract_mode = "relaxed"
# LuaRocks — Lua package registry.
#
# Used by neovim plugin managers (lazy.nvim, rocks.nvim) and by Lua
# tooling generally. Read-only HTTP fetches return rockspec metadata
# (text) and .rock archives (octet-stream).
[recipe]
name = "luarocks"
description = "LuaRocks Lua package registry"
[[host]]
domain = "luarocks.org"
methods = ["GET", "HEAD"]
content_types = ["application/json", "application/octet-stream", "text/plain"]
# npm public registry.
#
# Reads return JSON metadata or octet-stream tarballs. `npm publish`
# uploads octet-stream tarballs via PUT.
[recipe]
name = "npm-registry"
description = "npm registry contracts"
suggests = ["npm", "node"]
# Fake-secret swap: the sandbox gets a fake NPM_TOKEN; the proxy swaps in
# the real value only on egress to the npm_token-scoped registry below.
# See docs/refusals.md.
[network.dlp]
fake_secrets = [{ env = "NPM_TOKEN", credential = "npm_token" }]
[[host]]
domain = "registry.npmjs.org"
methods = ["GET", "HEAD", "PUT", "DELETE"]
content_types = ["application/json", "application/octet-stream"]
max_request_bytes = 524288000 # 500 MiB package upload cap
allow_credentials = ["npm_token"]
# OpenAI API.
#
# Chat completions, embeddings, files, fine-tuning. `multipart/form-data`
# permitted for the audio + files endpoints; everything else is JSON.
[recipe]
name = "openai"
description = "OpenAI API contracts"
# Fake-secret swap: the sandbox gets a fake OPENAI_API_KEY; the proxy swaps
# in the real value only on egress to the openai_key-scoped host below.
# See docs/refusals.md.
[network.dlp]
fake_secrets = [{ env = "OPENAI_API_KEY", credential = "openai_key" }]
[[host]]
domain = "api.openai.com"
methods = ["GET", "POST", "DELETE"]
content_types = ["application/json", "multipart/form-data"]
max_request_bytes = 26214400 # 25 MiB — OpenAI's file-upload cap
allow_credentials = ["openai_key"]
# OpenCode SaaS endpoints (Zen, auth, sharing) + GitHub Copilot proxy.
#
# `opencode.ai` hosts OpenCode's own services (auth, sharing).
# `api.githubcopilot.com` is the Copilot chat completion endpoint.
# `copilot-proxy.githubusercontent.com` is Copilot's CDN/proxy used
# during the OAuth flow.
[recipe]
name = "opencode-ai"
description = "OpenCode SaaS + GitHub Copilot endpoints"
[[host]]
domain = "opencode.ai"
methods = ["GET", "POST", "HEAD"]
content_types = ["application/json"]
[[host]]
domain = "api.githubcopilot.com"
methods = ["GET", "POST", "HEAD"]
content_types = ["application/json", "text/event-stream"]
allow_credentials = ["github_pat"]
[[host]]
domain = "copilot-proxy.githubusercontent.com"
methods = ["GET", "HEAD"]
contract_mode = "relaxed"
# PyPI + the index hosts behind it.
#
# `pypi.org` serves the JSON simple index (and the human site). The
# actual wheels/sdists are on `*.pythonhosted.org` (CDN backend), so
# pip and uv talk to both. `twine upload` POSTs multipart/form-data
# to `upload.pypi.org`.
[recipe]
name = "pypi"
description = "PyPI + pythonhosted contracts"
suggests = ["pip", "uv", "python"]
[[host]]
domain = "pypi.org"
methods = ["GET", "HEAD"]
content_types = ["application/json", "application/vnd.pypi.simple.v1+json"]
[[host]]
domain = "upload.pypi.org"
methods = ["POST"]
content_types = ["multipart/form-data"]
max_request_bytes = 1073741824 # 1 GiB — large wheels exist
[[host]]
domain = "*.pythonhosted.org"
methods = ["GET", "HEAD"]
# CDN endpoints don't carry credentials and the content types are
# heterogeneous (whl, tar.gz, sigs); relaxed mode keeps detection
# (BodyRaw) without enumerating every mime type.
contract_mode = "relaxed"
# static.rust-lang.org — rustup toolchain distribution.
#
# rustup downloads toolchains, components, and channel manifests from
# this CDN. Read-only (GET/HEAD); content set is heterogeneous (signed
# manifests, tar.xz, sha256 files), so relaxed-mode is appropriate.
[recipe]
name = "rust-lang"
description = "static.rust-lang.org rustup toolchain distribution"
[[host]]
domain = "static.rust-lang.org"
methods = ["GET", "HEAD"]
contract_mode = "relaxed"
# Slack API.
#
# Two content types: JSON for the modern API, multipart for legacy
# `files.upload`. `application/x-www-form-urlencoded` permitted for
# the older endpoints that still accept it (the Slack docs are
# explicit that JSON is preferred for new code).
[recipe]
name = "slack"
description = "Slack API contracts"
# Fake-secret swap: the sandbox gets a fake SLACK_BOT_TOKEN; the proxy
# swaps in the real value only on egress to the slack_token-scoped host
# below. See docs/refusals.md.
[network.dlp]
fake_secrets = [{ env = "SLACK_BOT_TOKEN", credential = "slack_token" }]
[[host]]
domain = "slack.com"
methods = ["GET", "POST"]
content_types = [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data",
]
max_request_bytes = 1073741824 # 1 GiB for files.upload
paths = ["/api/", "/oauth/"]
allow_credentials = ["slack_token"]
[[host]]
domain = "hooks.slack.com"
methods = ["POST"]
content_types = ["application/json"]
paths = ["/services/"]
# Stripe API.
#
# Stripe is one of the rare REST APIs that still uses
# `application/x-www-form-urlencoded` instead of JSON for request
# bodies. Allow both — newer endpoints accept JSON too.
[recipe]
name = "stripe"
description = "Stripe API contracts"
# Fake-secret swap: the sandbox gets a fake STRIPE_API_KEY / STRIPE_SECRET_KEY
# (a pattern-valid `sk_live_…`); the proxy swaps in the real value only on
# egress to the stripe_key-scoped hosts below. See docs/refusals.md.
[network.dlp]
fake_secrets = [
{ env = "STRIPE_API_KEY", credential = "stripe_key" },
{ env = "STRIPE_SECRET_KEY", credential = "stripe_key" },
]
[[host]]
domain = "api.stripe.com"
methods = ["GET", "POST", "DELETE"]
content_types = ["application/x-www-form-urlencoded", "application/json", "multipart/form-data"]
max_request_bytes = 1048576 # 1 MiB
allow_credentials = ["stripe_key"]
[[host]]
domain = "files.stripe.com"
methods = ["POST"]
content_types = ["multipart/form-data"]
max_request_bytes = 10485760 # 10 MiB receipts / IDs
allow_credentials = ["stripe_key"]
# Yarn classic registry mirror.
#
# `registry.yarnpkg.com` is Yarn's npm-compatible mirror. It serves
# the same JSON manifests + octet-stream tarballs as the npm registry.
[recipe]
name = "yarn-registry"
description = "Yarn classic registry mirror (npm-compatible)"
suggests = ["yarn", "node"]
[[host]]
domain = "registry.yarnpkg.com"
methods = ["GET", "HEAD"]
content_types = ["application/json", "application/octet-stream"]
max_request_bytes = 524288000 # 500 MiB
# Canister recipe for strict-mode execution of arbitrary binaries.
#
# Enables strict mode: any setup failure is fatal, seccomp uses
# KILL_PROCESS. No network access. Minimal filesystem (base.toml only).
# Intended for CI pipelines and production jobs where security is paramount.
#
# Usage:
# can run -r generic-strict -- ./my-binary --flag
# can run -r generic-strict -- cargo test
strict = true
[recipe]
name = "generic-strict"
description = "Strict no-network policy for untrusted binaries (CI/production)"
version = "2"
[filesystem]
deny = ["/etc/shadow", "/root", "/home"]
[network]
egress = "none"
[process]
max_pids = 64
env_passthrough = ["PATH", "LANG", "TERM"]
# These widen the kernel attack surface (debuggers, async I/O, multilib,
# self-sandboxing) for compiled-binary compatibility, so they are declared
# explicitly here rather than hidden in [syscalls] allow_extra — "strict"
# isolation must never silently grow the syscall surface.
[unsafe]
extra_syscalls = [
"ptrace",
"personality",
"seccomp",
"io_uring_setup",
"io_uring_enter",
"io_uring_register",
]
[recipe]
name = "gpg-public"
description = "gpg: public keyring + trustdb only; private keys denied (compose with `gpg-keyservers`)"
version = "2"
# Network access to public keyservers is NOT baked in. Compose:
#
# recipes = ["gpg-public", "gpg-keyservers"]
#
# and set `[sandbox.X.network] egress = "proxy"` in the manifest.
[filesystem]
read = [
"$HOME/.gnupg/pubring.kbx",
"$HOME/.gnupg/pubring.gpg",
"$HOME/.gnupg/trustdb.gpg",
"$HOME/.gnupg/gpg.conf",
"$HOME/.gnupg/gpg-agent.conf",
"$HOME/.gnupg/dirmngr.conf",
"$HOME/.gnupg/crls.d",
]
deny = [
"$HOME/.gnupg/private-keys-v1.d",
"$HOME/.gnupg/secring.gpg",
"$HOME/.gnupg/random_seed",
]
[process]
env_passthrough = [
"GNUPGHOME",
"GPG_AGENT_INFO",
"GPG_TTY",
]
# Canister recipe — ssh-agent (agent-mediated SSH)
#
# Lets a sandboxed process use the host's running ssh-agent for
# authentication WITHOUT exposing private key files. The agent socket
# is reached via SSH_AUTH_SOCK; any process that can talk to that
# socket can request signatures from the loaded keys but cannot
# read the key material.
#
# DOES NOT mount ~/.ssh — private keys never enter the sandbox. If a
# user genuinely needs on-disk keys (e.g., a tool that doesn't speak
# agent protocol), they must add an explicit `[filesystem] allow` entry
# in their canister.toml and accept the exposure.
#
# No `[[host]]` blocks: SSH is not HTTP and does not flow through the
# canister proxy. SSH egress is controlled by `[unsafe] reachable_ips`
# (or `[unsafe] unfiltered_egress`) on the containing sandbox.
#
# Note: the agent socket itself lives at a path the parent ssh-agent
# chose. The default canister filesystem isolation mounts /tmp, so if
# the socket is in /tmp (the common case) it's already reachable.
# Sockets in /run/user/<uid>/ require an explicit mount of that path.
[recipe]
name = "ssh-agent"
description = "agent-mediated SSH via SSH_AUTH_SOCK; no on-disk private keys"
version = "1"
[filesystem]
# Allow the known-hosts file so SSH host-key prompts don't recur. The
# host pubkey collection is not credential material.
read = [
"$HOME/.ssh/known_hosts",
"$HOME/.ssh/known_hosts.d",
"$HOME/.ssh/config",
]
# Defence in depth: even if a future recipe re-adds $HOME/.ssh wholesale,
# the private key globs stay denied. (deny wins over allow per the
# overlay.rs deny-wins rule.)
deny = [
"$HOME/.ssh/id_rsa",
"$HOME/.ssh/id_dsa",
"$HOME/.ssh/id_ecdsa",
"$HOME/.ssh/id_ed25519",
"$HOME/.ssh/id_xmss",
"$HOME/.ssh/identity",
]
[process]
env_passthrough = [
"SSH_AUTH_SOCK",
"SSH_AGENT_PID",
]
# Canister recipe — gh (GitHub CLI)
#
# Mounts `gh`'s non-credential config so the CLI knows the user's
# preferences (default editor, pager, host aliases). The OAuth token
# stored in ~/.config/gh/hosts.yml is denied so that a sandboxed
# process can't read it directly off disk.
#
# To use `gh` *with* authentication, pass the token via the GH_TOKEN /
# GITHUB_TOKEN env vars (env_passthrough handles this) rather than
# letting gh read it from hosts.yml. A sandboxed process can still
# observe the env var, but at least it can't read every authenticated
# host's token from disk.
#
# Network access to GitHub is NOT baked in. Compose with the `github`
# service recipe to allow api.github.com / github.com / raw + codeload:
#
# recipes = ["git", "gh", "github"]
#
# and set `[sandbox.X.network] egress = "proxy"` in the manifest.
[recipe]
name = "gh"
description = "gh: editor/pager/host config; hosts.yml token denied (use env vars)"
version = "1"
suggests = ["git", "github"]
[filesystem]
# We intentionally do NOT broadly mount $HOME/.config/gh because
# hosts.yml lives there and contains the OAuth token. Mount only the
# user preferences file. `gh` falls back to defaults if config.yml is
# absent on the host, so this is safe to declare.
read = [
"$HOME/.config/gh/config.yml",
]
deny = [
"$HOME/.config/gh/hosts.yml",
]
[process]
env_passthrough = [
"GH_TOKEN",
"GITHUB_TOKEN",
"GH_HOST",
"GH_REPO",
"GH_EDITOR",
"GH_PAGER",
"GH_BROWSER",
]
# Canister recipe — git
#
# Mounts the standard git configuration files so that `git` running inside
# the sandbox honours the user's identity, aliases, and global hooks. Does
# NOT mount credential stores — those would let the sandboxed process
# read forge tokens off disk. For private-repo HTTPS access, prefer SSH
# (compose with `ssh-agent` and rely on SSH_AUTH_SOCK rather than
# on-disk keys).
#
# No `[[host]]` blocks here: git remotes are user-defined
# (github.com, gitlab.com, an internal forge, …). Pair this recipe
# with a forge-specific one (e.g., `gh` for github.com) or add
# `[[host]] domain = "..."` entries to your canister.toml.
#
# Composes with: any recipe; meant to be one component of a larger sandbox
# (e.g., `recipes = ["git", "gh"]` in canister.toml).
[recipe]
name = "git"
description = "git: read user identity + aliases from ~/.gitconfig; credential helpers blocked"
version = "1"
[filesystem]
read = [
"$HOME/.gitconfig",
"$HOME/.config/git",
]
# Credential stores (file-backed or via credential helper output). Even
# though the user may have configured a credential helper that talks to
# the OS keychain rather than disk, the on-disk fallback files are still
# common; deny them so a recipe-level mistake can't leak the token.
deny = [
"$HOME/.git-credentials",
"$HOME/.config/git/credentials",
]
[process]
env_passthrough = [
"GIT_AUTHOR_NAME",
"GIT_AUTHOR_EMAIL",
"GIT_COMMITTER_NAME",
"GIT_COMMITTER_EMAIL",
"GIT_TERMINAL_PROMPT",
"GIT_CONFIG_NOSYSTEM",
]