Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Built-in Recipes

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.

Overview

container/

RecipeDescription
helmhelm: config + cache + data dirs; repository credentials via env
kubectlkubectl: config + cache; token files via KUBECONFIG env only
podman-configpodman: CLI config + containers dir; registry auth denied

core/

RecipeDescription
exampleExample recipe showing all available options

editors/

RecipeDescription
lazy-nvimlazy.nvim plugin manager (clones plugins via git; compose with neovim, git, github)
neovimNeovim editor: config + plugin dirs (no network — compose with github etc.)
opencodeOpenCode AI coding agent: scoped filesystem (no network — compose with LLM service recipes)

languages/

RecipeDescription
elixirElixir/Erlang runtime: BEAM syscalls + env (no network — compose with hex)
goGo toolchain: GOPATH + module cache + config (no proxy — compose with go-proxy)
nodeNode.js runtime (no registry access; compose with npm/pnpm/yarn for installs)
pythonPython 3 interpreter (no PyPI access; compose with pip for installs)

package-managers/

RecipeDescription
asdfasdf version manager (~/.asdf installs + shims)
cargoRust/Cargo toolchain: registry cache + config; credentials.toml denied (use CARGO_REGISTRIES_* env)
flatpakFlatpak applications
gnu-storeGNU Guix package manager (/gnu/store)
homebrewHomebrew/Linuxbrew package manager
misemise polyglot version manager (~/.local/share/mise + config)
nix-home-managerNix home-manager profile (~/.nix-profile + per-user state)
nixNix package manager (/nix/store)
npmnpm: cache + per-project config; ~/.npmrc denied (use NPM_TOKEN env)
pippip: cache + config; ~/.pypirc denied (use PIP_INDEX_URL env for private indexes)
pnpmpnpm: global store + config; auth tokens denied (use NPM_TOKEN env)
poetrypoetry: config + cache; auth.toml denied (use POETRY_HTTP_BASIC_* env)
rustuprustup: toolchain + update config; read-only except for toolchain installs
snapSnap package manager (/snap)
uvuv: cache + config; auth tokens via env
yarnyarn: cache + config; auth tokens denied (use YARN_NPM_AUTH_TOKEN env)

services/

RecipeDescription
anthropicAnthropic API contracts
awsAWS regional endpoint contracts
crates-iocrates.io Rust crate registry
githubGitHub REST + GraphQL API contracts
go-proxyGo module proxy + checksum database
gpg-keyserversPublic PGP keyservers (openpgp.org + Ubuntu)
hexHex.pm Elixir/Erlang package registry
huggingfaceHugging Face Hub contracts
luarocksLuaRocks Lua package registry
npm-registrynpm registry contracts
openaiOpenAI API contracts
opencode-aiOpenCode SaaS + GitHub Copilot endpoints
pypiPyPI + pythonhosted contracts
rust-langstatic.rust-lang.org rustup toolchain distribution
slackSlack API contracts
stripeStripe API contracts
yarn-registryYarn classic registry mirror (npm-compatible)

system/

RecipeDescription
generic-strictStrict no-network policy for untrusted binaries (CI/production)
gpg-publicgpg: public keyring + trustdb only; private keys denied (compose with gpg-keyservers)
ssh-agentagent-mediated SSH via SSH_AUTH_SOCK; no on-disk private keys

vcs/

RecipeDescription
ghgh: editor/pager/host config; hosts.yml token denied (use env vars)
gitgit: read user identity + aliases from ~/.gitconfig; credential helpers blocked

Recipe Contents

container/

container/helm.toml

[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",
]

container/kubectl.toml

[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",
]

container/podman-config.toml

[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",
]

core/

core/example.toml

# 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, …)

editors/

editors/lazy-nvim.toml

# 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",
]

editors/neovim.toml

# 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"]

editors/opencode.toml

# 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",
]

languages/

languages/elixir.toml

# 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",
]

languages/go.toml

# 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",
]

languages/node.toml

# 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",
]

languages/python.toml

# 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",
]

package-managers/

package-managers/asdf.toml

# 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",
]

package-managers/cargo.toml

# 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",
]

package-managers/flatpak.toml

# 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"]

package-managers/gnu-store.toml

# 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"]

package-managers/homebrew.toml

# 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"]

package-managers/mise.toml

# 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",
]

package-managers/nix-home-manager.toml

# 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",
]

package-managers/nix.toml

# 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"]

package-managers/npm.toml

# 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",
]

package-managers/pip.toml

[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",
]

package-managers/pnpm.toml

[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",
]

package-managers/poetry.toml

[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",
]

package-managers/rustup.toml

[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",
]

package-managers/snap.toml

# 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"]

package-managers/uv.toml

[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",
]

package-managers/yarn.toml

[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",
]

services/

services/anthropic.toml

# 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"]

services/aws.toml

# 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"]

services/crates-io.toml

# 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"

services/github.toml

# 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"

services/go-proxy.toml

# 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"]

services/gpg-keyservers.toml

# 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"]

services/hex.toml

# 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"

services/huggingface.toml

# 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"

services/luarocks.toml

# 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"]

services/npm-registry.toml

# 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"]

services/openai.toml

# 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"]

services/opencode-ai.toml

# 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"

services/pypi.toml

# 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"

services/rust-lang.toml

# 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"

services/slack.toml

# 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/"]

services/stripe.toml

# 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"]

services/yarn-registry.toml

# 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

system/

system/generic-strict.toml

# 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",
]

system/gpg-public.toml

[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",
]

system/ssh-agent.toml

# 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",
]

vcs/

vcs/gh.toml

# 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",
]

vcs/git.toml

# 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",
]