Agent Security Stack

The reason for this stack is simple: coding agents are useful, but you cannot let them run loose with live keys, weak dependency provenance, and broad ambient environment access. The security stack is bondage for launch policy, envchain-xtra for secret release, and nono for sandbox enforcement.

Last updated

Architecture
Reality check: this stack is still experimental. It is a practical local hardening pattern, not a finished security product. If you want the strongest isolation boundary for an agent, a dedicated VM is still better than a host-local CLI sandbox.
Diagram showing the agent security stack: shell wrapper to bondage to optional envchain-xtra to optional nono to exact pinned tool, with the core risks listed as live keys, dependency drift, and ambient environment access.
The point of the stack is not ceremony. It is to narrow trust around secrets, dependency drift, and host access.

1. Why this stack exists

By default, most agent setups inherit too much trust from the surrounding shell session. That means too many secrets, too much filesystem visibility, and too much confidence in whatever dependency tree happens to answer on PATH.

The actual reasons for hardening this are not abstract:

  • you do not want agents running loose with live API keys and other secrets
  • you cannot assume the dependency tree behind a familiar command is clean or stable
  • you do not want broad ambient access to shell state, config files, and local environment by default

The old pattern for hardening CLI agents was to stuff everything into shell wrappers: secret injection, Touch ID gates, sandbox flags, target resolution, and sometimes even binary verification. That can work, but it makes your shell script the real security boundary.

The cleaner model is to separate the jobs:

shell name → bondage → [envchain-xtra] → [nono] → exact pinned tool convenience launch policy secret release sandbox actual agent

That split matters because each layer addresses a different failure mode: bondage decides what exact artifact chain is allowed to launch, envchain-xtra decides when secrets are released, and nono decides what the launched process can actually touch.

It also deliberately leans on the OS where the OS is stronger than userland glue: macOS code-signing identity can be used as part of artifact approval, and Keychain remains the underlying secret store rather than re-implementing secret storage in the launcher.

If you want the implementation detail rather than the overview, start with agentbondage.org, then read the Getting Started and Trust Model docs.

2. Philosophy

This stack is opinionated. The philosophy is not “wrap everything in enough cleverness and call it secure.” It is to reduce ambient trust and make the risky decisions explicit.

  • Prefer explicit trust over ambient trust. Exact paths, exact hashes, explicit profiles, explicit secret release.
  • Use the OS for what the OS is good at. Keychain for secret storage, signing identity where available, kernel sandboxing for enforcement.
  • Keep layers narrow. Launch policy is not secret storage. Secret storage is not sandbox policy. Shell convenience is not a trust boundary.
  • Assume upgrades are security events. If a binary or package tree changed, re-approve it deliberately.
  • Make escape hatches obvious. If there is a no-sandbox path, it should be explicit and ugly, not a quiet profile drift.

In short: less magic, less ambient authority, more deliberate boundaries.

3. Layer responsibilities

LayerWhat it should doWhat it should not do
Shell wrapper Short names, prompt shaping, convenience aliases Decide which binary is trusted or which sandbox policy applies
bondage Verify exact targets, interpreters, package trees, and choose the launch policy Store secrets or enforce filesystem policy by itself
envchain-xtra Release secrets from Keychain-backed namespaces to one process tree Own the whole launch-policy story
nono Enforce filesystem, process, and network restrictions Resolve which launch artifact chain is trusted

4. Practical patterns

In practical terms: your secrets are already being shipped to frontier labs as tokens they can decode. Don't panic — you can hand this article to your coding agent and ask it to set the stack up for you.

The stack guide should explain the worldview, not carry every operational nuance inline. The sharp edges are better captured as repeatable patterns.

  • vendor-independence — keep instructions, plugins, tool access, and security layers portable across clients instead of baking the workflow into one vendor.
  • claude-to-codex-plugins — take a Claude-first plugin and make Codex first-class without forking the real workflow, using llm-wiki as the case study.
  • visual-inspection — use accessibility trees first, then a narrow localhost screenshot service when pixels actually matter.
  • sandbox-profiles — keep normal coding tight, make broader access explicit, and make true bypasses obvious.

These are the decisions that matter more than the exact command syntax: where the trust boundary lives, which capability gets isolated, and what counts as a real escape hatch.

5. Install

Quick bootstrap

If you want a working starter shape instead of hand-copying every profile, use agent-stack-bootstrap. It installs public templates and shell aliases, while keeping real local paths, fingerprints, hostnames, and tokens in private generated or ignored files.

git clone https://github.com/nvk/agent-stack-bootstrap.git
cd agent-stack-bootstrap
./install.sh
echo 'source "$HOME/.config/agent-stack/shell.zsh"' >> ~/.zshrc

The default install includes every optional public profile group. Pick narrower groups when you only need one path:

./install.sh --profiles spark
./install.sh --profiles ds4,pi-ds4
./install.sh --profiles none

The bootstrap stages bondage.conf.template; it does not install a live pinned ~/.config/bondage/bondage.conf. Render and pin that file locally after the target tools are installed.

Manual install

Start with the launcher and the sandbox:

brew tap nvk/tap
brew install nvk/tap/agent-bondage
brew install nono

If a profile needs Keychain-backed secret injection, add:

brew install nvk/tap/envchain-xtra
Important: `agent-bondage` is the formula name. It installs the bondage executable.

Useful references:

6. Recommended layout

Keep the pieces separate:

~/.config/bondage/bondage.conf
~/.bondage/tools/
~/.config/nono/profiles/
~/bin/

Good setups pin exact absolute artifacts, not PATH lookups or mutable global installs. For script-backed tools, that means pinning the entrypoint, the interpreter, and the package tree.

Minimal starter files

A non-doxing public starting point for the shared launcher config looks like this. Replace every absolute path and hash with values from your own machine.

# ~/.config/bondage/bondage.conf
[global]
envchain = /opt/homebrew/bin/envchain
envchain_fp = sha256:replace-me
nono = /opt/homebrew/bin/nono
nono_fp = sha256:replace-me
nono_profile_root = /Users/you/.config/nono/profiles
touchid = /opt/homebrew/bin/touchid-check
touchid_fp = sha256:replace-me
tool_root = /Users/you/.bondage/tools

[defaults "agent-nono"]
nono_allow_cwd = true
nono_allow_file = /dev/tty
nono_allow_file = /dev/null
nono_read_file = /dev/urandom

[defaults "github-env"]
env_command = GH_TOKEN=/opt/homebrew/bin/gh auth token
env_command = GITHUB_TOKEN=/opt/homebrew/bin/gh auth token

[defaults "codex-target"]
target_kind = native
target = /Users/you/.bondage/tools/codex/0.128.0/codex-aarch64-apple-darwin
target_fp = sha256:replace-me

[profile "codex"]
inherits = agent-nono,github-env,codex-target
use_envchain = false
use_nono = true
nono_profile = codex
touch_policy = none

Use [defaults "…"] blocks for repeated launch policy: common nono device grants, GitHub token injection, target paths, interpreter pins, and package-tree hashes. Each profile must opt in explicitly with inherits = …. Avoid one giant catch-all defaults block; keep defaults narrow enough that repin output tells you exactly what changed.

Then add one [profile "…"] block per client in bondage.conf and one matching JSON profile in ~/.config/nono/profiles/. The old fully-expanded profile format still works, but defaults make upgrades less brittle because a shared target pin can be repinned once instead of repeated across every tier.

A thin shell wrapper should look like this:

codex() {
  bondage exec codex ~/.config/bondage/bondage.conf -- "$@"
}

That wrapper is only naming sugar. The trust boundary is below it.

Keep home-shell startup boring

The login shell itself should not depend directly on a fragile repo symlink. Keep tiny stable bootstrap files in $HOME and let them source the real repo-backed config when it is readable.

# ~/.zshrc
if [ -r "$HOME/src-repo/.dotfiles/zsh/.zshrc" ]; then
  . "$HOME/src-repo/.dotfiles/zsh/.zshrc"
elif [ -r "$HOME/src-repo/.dotfiles/ai/shell.zsh" ]; then
  . "$HOME/src-repo/.dotfiles/ai/shell.zsh"
fi

That looks less clever than a direct symlink, but it fails much better. If the repo path is temporarily unreadable, you still have a stable entrypoint instead of silently falling back to a raw binary on PATH.

If you want concrete examples instead of the generic stack, jump to the tool-specific guides:

7. Operational hardening patterns

The architecture is only half the story. Local launcher stacks get brittle when upgrades, wrappers, and helper scripts are treated as harmless convenience instead of real operational policy.

  • Treat upgrades as security events. Re-pin the launcher after package-manager upgrades and verify the affected profiles before trusting the new toolchain.
  • Keep a named repair tier. A *-fix profile is different from *-unsafe and different again from *-rawdog. Repair access should be explicit and sandboxed where possible.
  • Test helper scripts. Denial helpers and restart suggestions need fixture tests for spaces, quotes, punctuation, and shell-heavy commands. A bad helper is worse than no helper.
  • Keep recovery and canonical state aligned. A temporary rescue path is fine. A permanent split between the rescue path and the real source of truth is not.

A practical post-upgrade loop is:

bondage doctor ~/.config/bondage/bondage.conf
bondage repin-globals ~/.config/bondage/bondage.conf
bondage doctor ~/.config/bondage/bondage.conf
bondage repin codex ~/.config/bondage/bondage.conf   # only if doctor suggests it
bondage verify codex ~/.config/bondage/bondage.conf
bondage chain codex ~/.config/bondage/bondage.conf -- --help

If repin updates a defaults block, every profile that inherits that block gets the new pin. That is the point; the command output should make the shared update scope visible. Then open a fresh login shell and confirm your wrapper names still resolve to the expected shell functions.

8. Declared Trust Assumptions

This stack only makes sense if you are explicit about what is trusted, what is not, and what you are responsible for maintaining over time.

Trust anchors

  • the OS secret store, especially Keychain on macOS, remains the real place secrets live
  • OS-level signing identity and local fingerprints are valid approval signals for launch artifacts
  • nono and the underlying kernel sandbox are more trustworthy than an agent's own self-reported sandbox mode
  • your local bondage.conf is a policy file you control and review deliberately

What this stack assumes

  • You will pin exact launch artifacts, not just friendly command names.
  • You will re-approve binaries and package trees after upgrades instead of silently inheriting drift.
  • You will treat secret release as a policy decision per profile, not as a convenience default.
  • You will keep shell wrappers thin and avoid moving trust decisions back into shell glue.
  • You will prefer immutable or versioned tool trees over mutable global installs whenever possible.

What this stack does not assume

  • It does not assume a familiar command implies a safe dependency tree.
  • It does not assume npm, Homebrew, or any installer has already solved supply-chain trust for you.
  • It does not assume the agent's own internal sandbox is enough.
  • It does not assume that a prompt injection will stay polite once the model has tool access.

For JS-heavy agents, the real trust object is not the shell command name. It is the launch artifact set:

  • the exact entrypoint file
  • the exact interpreter
  • the exact package tree

9. What this does not solve

This is not a complete supply-chain solution, and it does not magically make agent dependencies trustworthy.

If you bless a compromised artifact tree, you only get a perfectly preserved compromise.

The stack is good at launch-time integrity and runtime containment. It is not magic at acquisition time. That means:

  • global npm installs are still a weak place to start from
  • mutable tool trees are worse than immutable versioned bundles
  • `rawdog` is a real escape hatch and should be treated as such
  • Touch ID gates are local approval signals, not remote attestation
  • a host-local sandbox is still weaker than a dedicated VM boundary

10. Verify before you trust it

Before wiring a tool into your shell, verify the profile explicitly:

bondage verify codex ~/.config/bondage/bondage.conf
bondage chain codex ~/.config/bondage/bondage.conf -- --help
bondage exec codex ~/.config/bondage/bondage.conf -- --help

The sequence matters:

  1. verify confirms the pinned artifacts match what you intended
  2. argv lets you inspect the exact launch chain without executing it
  3. exec proves the real path works

If you need one sentence for the model: keep shell convenience thin, put launch policy in bondage, let envchain-xtra release secrets, and let nono sandbox the result.