Quick start
Three commands to go from clone to first protected agent run.
# 1. clone & build (private alpha - see Installation for the GOPRIVATE setup) $ git clone git@github.com:paanSinghCoder/troopr.git $ cd troopr && go build -o /usr/local/bin/troopr ./cmd/troopr # 2. sanity check $ troopr version 0.1.0 # 3. run your agent inside the sandbox $ cd ~/code/my-project $ troopr run --profile standard -- claude
That's it. Your agent runs exactly as it always has - same stdio,
same environment, same arguments. The only difference: reads of
.env, ~/.ssh, ~/.aws/credentials
and the rest of the universal denylist now
return Operation not permitted.
Installation
Requirements
- macOS 13 or newer (Apple Silicon or Intel)
- Go 1.22+ (only required to build from source)
- A working
sandbox-execat/usr/bin/sandbox-exec(ships with macOS)
One-time setup for the private module
The GitHub repo is currently private. Until it's public, both
git clone and go install need authenticated
access to github.com/paanSinghCoder/troopr.
# tell Go not to route this module through the public proxy / sumdb $ go env -w GOPRIVATE=github.com/paanSinghCoder/* $ go env -w GONOSUMCHECK=github.com/paanSinghCoder/* # make Go use SSH for github.com clones $ git config --global url."git@github.com:".insteadOf "https://github.com/"
You'll also need an SSH key loaded and attached to a GitHub account
with read access to the repo. (Or a personal access token with
repo scope in ~/.netrc, if you prefer
HTTPS.)
Build from source
$ git clone git@github.com:paanSinghCoder/troopr.git $ cd troopr $ go build -o /usr/local/bin/troopr ./cmd/troopr
The output is a single static binary of about 5MB. No runtime dependencies - it's pure Go standard library plus cobra (CLI parser) and yaml.v3 (config loader).
Verify
$ troopr version 0.1.0 $ troopr check --profile standard | head -3 # Resolved config profile: standard cwd: ...
Your first sandbox
Before pointing troopr at a real project, run a 30-second smoke test to confirm the deny is actually working on your machine.
$ mkdir /tmp/troopr-demo && cd /tmp/troopr-demo $ echo "SECRET=hunter2" > .env $ echo "ok" > public.txt # reading a normal file works $ troopr run --profile standard -- /bin/cat public.txt ok # reading .env is blocked $ troopr run --profile standard -- /bin/cat .env cat: .env: Operation not permitted
If the second command prints hunter2 instead of the
permission error, see Troubleshooting.
Commands
Five subcommands. Run troopr --help for the in-CLI
version.
troopr run
troopr run [--profile <name>] [--config <path>] -- <agent-cmd> [args...]
Launch a command inside a sandbox. Everything after -- is passed through to the wrapped binary verbatim.
troopr init
troopr init
Write a sample troopr.yaml to the current directory. Will not overwrite an existing file.
troopr check
troopr check [--profile <name>] [--config <path>]
Dry run. Prints the resolved configuration and the sandbox profile (.sb) that would be generated, without executing anything.
troopr log
troopr log [-f|--follow]
Tail ~/.troopr/log.jsonl. Each line is a JSON record of a denied filesystem read or write.
troopr version
troopr version
Print the installed troopr version.
Flags accepted by run and check
| Flag | Type | Default | Effect |
|---|---|---|---|
--profile | string | standard | One of strict, standard, permissive. Overrides the profile named in troopr.yaml, if any. |
--config | path | ./troopr.yaml | Explicit path to the config file. If omitted, troopr looks for troopr.yaml or troopr.yml in the current directory. |
Configuration
troopr.yaml lives at the root of your project. Generate
a starter file with troopr init.
Schema
# troopr.yaml version: 1 # required, must be 1 profile: standard # strict | standard | permissive deny: # patterns added on top of the profile - "**/.env*" - "**/*.pem" - "~/.ssh/**" - "src/internal/secrets/**" allow: # carve-outs from deny - "**/.env.example"
Fields
| Field | Required | Notes |
|---|---|---|
version | yes | Must be 1. Anything else fails with exit code 1. |
profile | no | Default standard. A --profile flag overrides this. |
deny | no | List of path globs to add to the profile's baseline denylist. Order doesn't matter. |
allow | no | List of path globs that are explicitly permitted even if matched by a deny rule. |
Glob syntax
| Token | Matches | Example |
|---|---|---|
** | Any sequence of characters, including /. | **/.env → /foo/bar/.env |
* | Any sequence of characters except /. | *.pem → key.pem |
? | A single non-/ character. | file?.txt → file1.txt |
~ / $HOME | Expanded to your home directory before matching. | ~/.aws/** → /Users/you/.aws/foo |
Merge precedence
When troopr starts, it merges configuration in this order - later sources override earlier ones:
- The profile baseline (universal denylist + profile-specific allows)
- Anything in
./troopr.yamlor the file passed to--config - The
--profileflag, if present, overrides the profile named in the config file
The deny and allow lists from your config
are added to the profile baseline; they don't
replace it. So permissive with a config file is still
permissive + your extras, not just your extras.
Profiles
Three baselines that determine what the agent can read and whether it can reach the network. Every profile applies the universal denylist.
| Profile | Reads outside CWD | Writes | Network | Universal denylist |
|---|---|---|---|---|
strict | system libraries only | CWD + macOS temp dirs | blocked | enforced |
standard | $HOME + system | $HOME + CWD + temp | allowed | enforced |
permissive | everywhere | everywhere | allowed | enforced |
strict
The most restrictive profile. The agent can read its working
directory and the system libraries it needs to start
(/usr/lib, /System, /usr/bin,
/bin, and a few others). It cannot read anywhere else
under your home directory, cannot reach the network, and cannot
write anywhere outside the CWD and macOS temp directories. Use this
when running an unfamiliar agent unattended.
standard
The default. The agent has full read and write access under
$HOME, plus the system. Network is allowed. The
universal denylist is still enforced, so ~/.aws/credentials,
~/.ssh/**, and similar paths remain unreadable. This is
the right profile for day-to-day use with trusted agents like Claude
Code or Cursor.
permissive
Filesystem and network access are unrestricted except
for the universal denylist and anything you add via the
deny list. Use this when you want minimal interference
and just need the safety net of "never let it read my AWS keys."
Universal denylist
Seventeen patterns that are blocked in every
profile, including permissive. Add more in your
troopr.yaml under deny:.
| Pattern | Why it's blocked |
|---|---|
**/.env | dotenv files at any depth |
**/.env.* | .env.local, .env.production, etc. |
**/*.pem | PEM-encoded keys and certs |
**/*.key | raw private keys |
**/id_rsa* | OpenSSH RSA keypairs |
**/id_ed25519* | OpenSSH Ed25519 keypairs |
~/.ssh/** | everything under your SSH directory |
~/.aws/credentials | AWS access keys |
~/.aws/config | AWS profile + region config |
~/.gnupg/** | GPG keyrings and trust DB |
~/.kube/config | Kubernetes cluster credentials |
~/.docker/config.json | Docker registry auth tokens |
~/.netrc | legacy HTTP basic auth store |
~/Library/Keychains/** | macOS Keychain database files |
**/secrets/** | anything in a secrets/ directory |
**/*.kdbx | KeePass 2 password databases |
**/*.kdb | KeePass 1 password databases |
If your stack stores secrets somewhere unconventional (a
~/.config/myapp/ directory, a non-standard cloud config
path, a custom token cache), add it to your
deny list explicitly. The universal denylist
covers the common cases; it cannot anticipate yours.
Recipes
Concrete one-liners for the agents people actually use.
Claude Code
$ cd ~/code/my-project $ troopr run --profile standard -- claude
Cursor CLI
$ troopr run --profile standard -- cursor-agent Codex CLI
$ troopr run --profile strict -- codex Aider
$ troopr run --profile standard -- aider An arbitrary script or binary
$ troopr run --profile strict -- ./my-script.sh --flag value $ troopr run -- node ./scripts/refactor.js
Everything after -- is passed to sandbox-exec
verbatim. Flags, environment variables, and stdio all work exactly
as they would outside troopr.
Audit log
troopr tails the macOS unified log for sandbox denials and writes
each one as a JSON line to ~/.troopr/log.jsonl.
Format
{"timestamp":"2026-05-14T03:30:26Z","agent":"claude","path":"/Users/g/.aws/credentials","action":"deny"} | Field | Value |
|---|---|
timestamp | RFC3339 UTC. |
agent | The first argument passed after -- (typically the agent binary name). |
path | The absolute path the kernel denied access to. |
action | Always "deny" in the current version. |
Watching it live
$ troopr log -f How it's collected
troopr spawns log stream --predicate 'eventMessage CONTAINS
"Sandbox:" AND eventMessage CONTAINS "deny"' in the background
while your agent runs. Each matching line is parsed and appended to
~/.troopr/log.jsonl. This is best-effort - if the unified
log stream lags or drops events, the JSONL will lag too. Treat the
audit log as a debug aid, not a security-of-record audit trail.
Exit codes
| Code | Meaning |
|---|---|
0 | Success. |
1 | Configuration error. Invalid YAML, unknown profile, unknown key. |
2 | Sandbox setup error. Could not exec sandbox-exec, profile file could not be written, etc. |
n | Exit code of the wrapped command, propagated verbatim. A signal-killed child returns 128 + signal number. |
Troubleshooting
"sandbox-exec: command not found"
You're not on macOS, or your shell can't see /usr/bin.
troopr only runs on macOS 13+; there is no Linux or Windows build.
The agent works outside troopr but fails under strict
Most agents read configuration from ~/.config/<tool>/
or ~/.cache/<tool>/ on startup. strict
blocks everything outside the CWD. Switch to standard
and add specific deny patterns for the secrets you care
about.
Reads of .env succeed when they shouldn't
Check that your config actually loaded. Run
troopr check --profile standard and confirm the deny
regex for .env appears in the generated profile. If you
have a troopr.yaml with an explicit allow:
entry that matches your file, the carve-out wins. Carve-outs are
emitted after denies precisely so they can override.
"Operation not permitted" on files I want the agent to read
You're probably in strict. Either move to
standard, or add an allow: rule for the
path. If the path lives outside CWD and outside $HOME,
even standard won't reach it - switch to
permissive.
The audit log at ~/.troopr/log.jsonl is empty
The log stream background process needs a moment to
start. Very short-lived agent runs may finish before the first deny
event lands. Try the command again, or watch
log show --predicate 'eventMessage CONTAINS "Sandbox:"'
--last 1m to confirm denials are firing at the kernel level
regardless.
Known limitations
- macOS only. Apple's
sandbox-exechas no Linux or Windows equivalent. sandbox-execis deprecated by Apple. It still ships on macOS 13–15 and works reliably; a future macOS release may force a migration to a different isolation mechanism.- Path-based, not content-based. A secret stored at an unusual path is readable until you add the path to
deny:. - One profile per
troopr run. There is no way to apply different rules to subprocesses spawned by the agent - they inherit the parent's policy. - Not a security boundary against an adversarial binary. troopr is defense-in-depth against honest mistakes (the agent reaching for
~/.aws/credentialsbecause the LLM suggested it). A program actively trying to escape can probe TCC-permitted services, kernel bugs, or paths still reachable viamach-lookup. - Audit log is best-effort. It tails
log streamand can miss events under load.