-
-
Notifications
You must be signed in to change notification settings - Fork 2
Sandboxes
Coyote can launch itself inside an isolated Docker Sandbox (sbx) with one command. This gives you a disposable,
hypervisor-isolated environment where Coyote runs against your project workspace without having access to the rest of
your host machine.
Sandbox mode is powered by Docker Sandboxes. Coyote does not implement its own
container layer. It delegates everything to the sbx CLI: lifecycle, isolation, networking, image management, and
process supervision. Coyote's only job is to orchestrate the bootstrap (build a sandbox of the right shape, copy your
config + vault credentials in, attach you to a running session) and then get out of the way.
Install the sbx CLI first (Docker Sandboxes install guide). Then from any directory:
# Sandbox named after the current directory's basename
coyote --sandbox
# Or with an explicit name
coyote --sandbox my-project
# Create without copying your host config (clean-slate sandbox)
coyote --sandbox throwaway --fresh
# Skip all sbx mixin discovery and application (debugging / minimum sandbox)
coyote --sandbox --no-mixins
# True bare-bones, isolated sandbox with fresh Coyote install
coyote --sandbox --fresh --no-mixinsThe first run takes a few minutes (building the Coyote sandbox image, installing Rust/uv/build deps, compiling
coyote-ai). Subsequent attaches to the same sandbox are instant: Your config, vault state, sessions, OAuth tokens,
and installed tools persist inside the sandbox until you sbx rm it.
Re-running coyote --sandbox [NAME] with the same name re-attaches to the existing sandbox silently rather than
creating a fresh one. --fresh and --no-mixins are ignored on re-attach (they only affect sandbox creation).
Pass a distinct name for each sandbox you want to keep alive against the same directory. Coyote will treat them as fully independent sbx sandboxes. This is handy for branch-per-sandbox workflows, spike vs. main, or running two Coyote sessions with different vault providers side by side:
# From ~/code/my-project
coyote --sandbox feature-branch # create/attach to sandbox 'feature-branch'
coyote --sandbox spike # create/attach to sandbox 'spike'
coyote --sandbox review # create/attach to sandbox 'review'sbx ls shows each one. Manage them individually with sbx stop <NAME>, sbx rm <NAME>, etc. The bare
coyote --sandbox (no name) still resolves to the current directory basename, so treat that as your default sandbox
and use explicit names for the rest.
Tip: Inside the sandbox REPL, prefix any line with
!to run a shell command without going throughsbx exec <name> -- <cmd>to modify the sandbox state; .e.g,!apt-get update,!git pull,!cargo build, etc. Output streams to your terminal, Ctrl-C interrupts long-running commands, and you don't spend any tokens because no output is sent to the LLM. See REPL -!<command>for details.
Coyote bundles a pre-built sbx kit (its sandbox manifest) directly inside the binary. When you run --sandbox,
it executes this sequence:
# 1. Extract the embedded base kit to your local cache (skipped on hash-match)
coyote --info | grep -i "sbx_kit_dir" | awk '{print $2}'
# 2. Discover mixins:
# - Walk known discovery paths for user-authored sbx-mixin.yaml files
# - Inspect your secrets_provider type; if non-Local, also extract the
# matching built-in vault-provider mixin to:
# $XDG_CACHE_HOME/coyote/sbx-vault-mixins/<provider>/
# 3. Log what's about to be applied (info! and println!). See "Verbose
# mixin log" below.
# 4. Check if a sandbox with this name already exists
sbx ls
# 5. If not, create it with the base kit + every discovered mixin layered on
sbx create \
--name <NAME> \
--kit <cache>/sbx-kit/ \
--kit <vault-provider-mixin-if-any> \
--kit <user-mixin-1> \
--kit <user-mixin-N> \
coyote .
# 6. Copy your host config into the sandbox (skipped if --fresh). Each top-level
# entry is copied individually to sidestep a macOS `docker cp` quirk that
# silently drops files carrying `com.apple.provenance` xattrs when they're
# tarred as part of a recursive directory copy.
sbx exec <NAME> sh -c "sudo mkdir -p /home/agent/.config/coyote && sudo chown agent:agent /home/agent/.config/coyote"
for entry in ~/.config/coyote/*; do
sbx cp "$entry" <NAME>:/home/agent/.config/coyote/
done
sbx exec <NAME> sh -c "sudo chown -R agent:agent /home/agent/.config/coyote"
# 7. Copy your vault password file, if a local provider is configured (skipped if --fresh)
sbx exec <NAME> sh -c "sudo mkdir -p <parent> && sudo chown agent:agent <parent>"
sbx cp <host-password-file> <NAME>:<destination>
sbx exec <NAME> sh -c "sudo chown -R agent:agent <destination>"
# 8. Hand control to sbx (Coyote's process is replaced). `--kit` is re-passed
# on reattach because sbx expects it even when the sandbox already exists.
exec sbx run --name <NAME> --kit <cache>/sbx-kit/Once sbx run takes over, Coyote on the host exits and your terminal is connected to Coyote inside the sandbox. All
signals (Ctrl-C, etc.) flow straight through.
Coyote handles steps 1–3, 6, 7, and the
--nameargument of step 5. Everything else issbxdoing its job.
Before sbx create runs, Coyote emits a single block to both the log and stdout naming every mixin about to be applied:
Applying 3 sbx mixin(s):
<built-in: vault-one_password> (adds: 1 install, 6 domains)
~/.config/coyote/functions/sbx-mixin.yaml (adds: 0 installs, 20 domains)
~/.config/coyote/agents/my-python-dev/sbx-mixin.yaml (adds: 1 install, 1 domain)
If zero mixins were discovered, you'll see No sbx mixins discovered. in the log (no terminal noise). If you launched
with --no-mixins, you'll see Mixin discovery disabled via --no-mixins. instead.
Skim this log on first launch and after installing any shared configuration bundle. It's your audit point for what each mixin grants in terms of installs and network domain allowances.
Coyote intentionally does not wrap sandbox lifecycle commands. Use sbx directly as it's the single source of truth:
| Task | Command |
|---|---|
| List sandboxes | sbx ls |
| Open a shell in a running sandbox | sbx exec <NAME> |
| Run a one-off command in a sandbox | sbx exec <NAME> <command> |
| Copy files in/out |
sbx cp <src> <dest> (use <NAME>:/path to reference a sandbox) |
| Stop a sandbox without removing it | sbx stop <NAME> |
| Remove a sandbox (destroys all state) | sbx rm <NAME> |
| Forward a port to the host | sbx ports <NAME> |
| Diagnose problems | sbx diagnose |
Run sbx --help for the full surface. None of these are reimplemented in Coyote.
Coyote's sandbox bootstrap is aware of your vault configuration and behaves differently depending on which provider you've chosen:
Coyote resolves your vault_password_file (or secrets_provider.password_file) from your host config and copies it into the sandbox at the matching location. The path is rewritten when it lives under $HOME (so ~/.coyote_password on your host becomes /home/agent/.coyote_password in the sandbox), but kept verbatim for absolute paths outside $HOME (so /etc/coyote/.coyote_password is copied to /etc/coyote/.coyote_password inside the sandbox). No further action is needed — your vault works on first launch.
Additionally, at vault initialization time inside the sandbox, Coyote auto-retranslates any vault_password_file (or secrets_provider.password_file) that points to a host home path (/home/<X>/..., /Users/<X>/..., or C:\Users\<X>\...) to the corresponding /home/agent/... path. This means custom password-file locations under your host's home directory (e.g. /home/atusa/.config/coyote/.password, /Users/atusa/..., or C:\Users\atusa\...) resolve correctly in the sandbox without any config rewriting — your config stays pristine. An INFO-level log line is emitted whenever this translation kicks in, so you can verify the resolution if needed.
| Host OS | Password file under $HOME/%USERPROFILE%
|
Password file outside $HOME/%USERPROFILE%
|
|---|---|---|
| Linux | Copied to /home/agent/<rel>
|
Copied verbatim (e.g. /etc/coyote/.password -> /etc/coyote/.password) |
| macOS | Copied to /home/agent/<rel>
|
Copied verbatim |
| Windows | Copied to /home/agent/<rel> (with backslashes normalized to forward slashes) |
Refused with a clear error. Windows paths outside %USERPROFILE% (e.g. C:\Program Files\Coyote\vault.txt) can't be projected into a Linux sandbox. Move the file under your user profile (C:\Users\<you>\...). |
If secrets_provider.type is anything other than local, Coyote automatically applies a built-in mixin that
installs the corresponding provider CLI inside the sandbox. The mixin also allowlists the domains the CLI needs to
authenticate:
| Provider | CLI installed | Auto-applied mixin |
|---|---|---|
one_password |
op |
<built-in: vault-one_password> |
azure_key_vault |
az |
<built-in: vault-azure_key_vault> |
gopass |
gopass |
<built-in: vault-gopass> |
aws_secrets_manager |
aws (v2) |
<built-in: vault-aws_secrets_manager> |
gcp_secret_manager |
gcloud |
<built-in: vault-gcp_secret_manager> |
The mixin installs the CLI but does not log you in — see the next section.
After the sandbox is created, you must authenticate the provider CLI inside the sandbox once. Either use the built-in
REPL command passthrough to execute the login command in the sandbox
when already open to the REPL, or use the Docker sbx CLI directly via sbx exec <NAME> <command>:
| Provider | First-time sandbox auth |
|---|---|
local |
Nothing (password file auto-copied) |
one_password |
|
azure_key_vault |
|
gopass |
|
aws_secrets_manager |
|
gcp_secret_manager |
|
These commands authenticate once per sandbox, meaning credentials persist on the sandbox filesystem until you
sbx rm <NAME>.
If you forget to re-auth, the vault will fail to decrypt the first time Coyote needs a secret inside the sandbox, and
you'll see a clear error from gman pointing at the right login command.
If you'd rather not re-authenticate inside the sandbox, you can transfer your host credentials directly:
sbx cp ~/.aws <NAME>:/home/agent/.aws
sbx cp ~/.config/gcloud <NAME>:/home/agent/.config/gcloudThis is faster but bleeds host state into the sandbox. It's your call. Coyote intentionally doesn't do this automatically.
To add applications, network allowances, environment variables, or files to your sandbox beyond what the base kit
provides, drop an sbx-mixin.yaml file at any of these locations and Coyote will discover and apply it automatically
on every coyote --sandbox:
| Discovery path | Purpose |
|---|---|
<config-dir>/sbx-mixin.yaml |
Top-level user mixin. Applies to every sandbox you launch |
<config-dir>>/functions/sbx-mixin.yaml |
Mixin for global custom tools |
<config-dir>/functions/<tool>/sbx-mixin.yaml |
Per-custom-tool mixin (alphabetical) |
<config-dir>/agents/<agent>/sbx-mixin.yaml |
Per-agent mixin (alphabetical), applied for every sandbox |
<workspace-root>/.coyote/sbx-mixin.yaml |
Workspace mixin (walk-up search from cwd) |
Coyote does not recursively scan for
sbx-mixin.yamlanywhere else. These five paths are the whole surface, meaning anything outside is ignored.
The <workspace-root> walk follows the same convention as Memory: Coyote walks up from your current directory
looking for the first ancestor containing .coyote/sbx-mixin.yaml. Use this to ship per-project sandbox extensions in
your repo.
Mixins are standard sbx kit YAML with kind: mixin. Here's a complete working example that adds ruff to every
sandbox:
# <config-dir>/sbx-mixin.yaml
schemaVersion: "1"
kind: mixin
name: my-python-tooling
description: Install the ruff Python linter for use in any sandbox
network:
allowedDomains:
- "files.pythonhosted.org:443"
- "pypi.org:443"
commands:
install:
- command: "uv tool install ruff"
user: "1000"
description: Install ruff via uvAfter saving this file, your next coyote --sandbox automatically applies it. See the official sbx kit reference
for the full mixin schema.
Coyote already ships mixins for the most common needs. They get auto-applied whenever they're relevant — you don't need to do anything to enable them:
-
Built-in tools mixin (auto-applied when
coyote --install functionshas run). Allowlists the domains used by every built-in global tool and the default MCP server set: Wikipedia, arxiv, jina, wttr, WolframAlpha, Perplexity, Tavily, Twilio, github MCP, atlassian MCP, ddg-search MCP, npm registry, Docker registries. -
Vault provider mixins (auto-applied when
secrets_provider.typematches). See Vault Behavior above. One for each of 1Password, Azure, gopass, AWS, and GCP.
You can list everything that's about to be applied via the verbose mixin log on every launch.
A mixin in <config-dir>/agents/<agent>/sbx-mixin.yaml travels with the agent if you publish it via the Sharing Configurations
mechanism. See that page for the security implications. Installing a bundle that ships an sbx-mixin.yaml grants the
included install commands and network domains the next time you coyote --sandbox.
If you want to point Coyote at a completely different kit instead of the embedded one (e.g. for development, hardening,
or a fork), set COYOTE_SANDBOX_KIT:
COYOTE_SANDBOX_KIT=./my-fork-of-coyote-kit/ coyote --sandboxWhen this environment variable is set, Coyote skips the embedded-kit extraction entirely and passes the override path
straight to sbx. Use this sparingly. The embedded kit is what every documented coyote --sandbox workflow assumes.
Sandbox mode refuses to start a sandbox if $IS_SANDBOX is already set. The bundled kit exports IS_SANDBOX=1 inside
every Coyote sandbox, so running coyote --sandbox from within a sandbox is a no-op error because you're already in one.
If something looks wrong, these are your three debugging flags:
| Flag | Effect | Use when… |
|---|---|---|
--no-mixins |
Skips all mixin discovery (built-in vault + user-authored) | A mixin is causing sbx create to fail and you want to bisect |
--fresh |
Skips host config + vault password file copy | You suspect your host config is contaminating the sandbox; isolation test |
COYOTE_SANDBOX_KIT |
Points at an alternate base kit instead of the embedded one | Developing a fork of the kit or hardening for a specific environment |
-
A mixin causes
sbx createto abort. Re-run with--no-mixinsto confirm the base kit works. Then check the verbose mixin log to identify which mixin was being applied when it failed. Open that mixin file and inspect itscommands.installblock. -
A malformed
sbx-mixin.yamlaborts launch beforesbx createruns. Coyote fails fast with a parse error naming the file. Fix the YAML or temporarily move it aside. - Vault decryption fails inside the sandbox. You forgot to re-auth your non-Local provider. See Re-Authenticating Your Vault in a Sandbox.
-
A custom tool fails with
command not found. The tool's required binary isn't installed in the sandbox. Add it to the matching mixin (per-tool, per-agent, or top-level user mixin). See Custom Tools. -
A network request from inside the sandbox hangs or fails. The domain isn't in any
allowedDomainsblock. Add it to a user mixin. The sandbox's proxy denies all unlisted traffic silently.
- You only need filesystem isolation, not network or process isolation. sandbox mode trades a few minutes of first-run setup and ongoing VM overhead (one Docker daemon per sandbox) for full hypervisor isolation. If you only want to restrict which files Coyote can touch, the existing per-tool permission model is lighter weight.
-
Fast one-off shell commands. Sandbox attach is fast on warm sandboxes, but cold-start (image + kit installs)
takes minutes. Plain
coyote --execute "..."on the host is faster for trivial work. - You need to interact with host-only resources (the host's clipboard, host system services, certain GUI integrations). Sandboxes can't see the host beyond the mounted workspace and the proxy-mediated network.
- Docker Sandboxes: Official Docs
- Vault: How Coyote resolves the password file that gets copied in
-
Environment Variables: Including
COYOTE_SANDBOX_KIT - Clients: Auth flows you may need to redo inside the sandbox for OAuth providers