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 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.
Agent flags pass through: troopr run agent --model auto.
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 | head -5 # Resolved config cwd: ... home: ... deny:
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 /bin/cat public.txt ok # reading .env is blocked $ troopr run /bin/cat .env cat: .env: Operation not permitted
If the second command prints hunter2 instead of the
permission error, see Troubleshooting.
Commands
Seven subcommands. Run troopr --help for the in-CLI
version.
troopr run
troopr run [--config <path>] <cmd> [args...]
Launch a command inside the sandbox. Agent flags pass through - e.g. troopr run agent --model auto.
troopr login
troopr login [--agent <binary>]
Log in to your agent. Runs outside the deny-list wrapper so auth flows work normally.
troopr init
troopr init
Write a sample troopr.yaml to the current directory. Will not overwrite an existing file.
troopr config init
troopr config init
Write a sample ~/.troopr/config.yaml with global deny/allow defaults.
troopr check
troopr check [--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 |
|---|---|---|---|
--config | path | ./troopr.yaml | Explicit path to the config file. If omitted, troopr looks for troopr.yaml or troopr.yml in the current directory. |
Everything else on the run line is passed to the wrapped
command. Use troopr run agent --model auto, not
troopr run --model auto agent - troopr only parses
--config.
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 deny: # patterns added on top of the built-in denylist - "**/.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. |
deny | no | List of path globs added to the built-in universal 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 add to earlier ones:
- The built-in universal denylist
- Anything in
~/.troopr/config.yaml(fromtroopr config init) - Anything in
./troopr.yamlor the file passed to--config
The sandbox policy is (allow default) plus deny rules for
every merged pattern. allow carve-outs override matching
deny entries.
Universal denylist
Fifteen patterns always blocked on every troopr run.
Add more in your troopr.yaml or
~/.troopr/config.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 |
**/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 |
**/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 claude
Cursor agent
$ troopr run agent --model auto Codex CLI
$ troopr run codex Aider
$ troopr run aider Agent login (outside sandbox)
$ troopr login $ troopr login --agent claude
An arbitrary script or binary
$ troopr run ./my-script.sh --flag value $ troopr run node ./scripts/refactor.js
Agent flags go after the command name. troopr only parses
--config; everything else is passed to
sandbox-exec verbatim.
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 key, missing file. |
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.
Reads of .env succeed when they shouldn't
Check that your config actually loaded. Run
troopr check and confirm the deny regex for
.env appears in the generated sandbox 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
Add an allow: rule for the path in
troopr.yaml or ~/.troopr/config.yaml.
If the path is on the universal denylist, the carve-out must match
it exactly.
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 policy 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.