Infrastructure for sandboxed agentic coding: sandbox tooling, credential injection, and user-specific rules. Not a library or app.
rules.md # Cross-repo agent conventions (referenced by multiple tools)
safe.sh # Safehouse sandbox wrapper
credential-server # Python credential server (runs outside sandbox)
bin/gh # gh wrapper (runs inside sandbox)
bin/aws # aws wrapper (runs inside sandbox)
bin/az # az wrapper (runs inside sandbox)
bin/ssh # ssh wrapper — gates SSH agent access (runs inside sandbox)
bin/docker # docker wrapper -- gates access via socket proxy (runs inside sandbox)
bin/uv # uv wrapper -- enables keyring auth via credential-server (runs inside sandbox)
bin/pip # pip wrapper -- enables keyring auth via credential-server (runs inside sandbox)
bin/keyring # keyring CLI bridge for uv/pip netrc credentials (runs inside sandbox)
bin/git-credential-helper # git credential helper (runs inside sandbox)
bin/osascript # osascript wrapper (neutered inside sandbox)
remote/aws # aws CLI wrapper for a remote host (vendored downstream)
remote/aws-credential-process # credential_process helper for remote SDKs (vendored downstream)
remote/open-url # URL opener for a remote host -- opens in the Mac browser (vendored downstream)
Commit after completing work. Ask before committing if unsure whether the change belongs in the repo.
This is a public repo. Never commit sensitive details (tokens, passwords, internal hostnames, account IDs, etc.) without asking first.
None. Verify changes manually:
./credential-server &
./safe.sh gh auth status
./safe.sh aws --profile <profile> sts get-caller-identity#!/bin/bashwithset -eu- Quote all variables:
"$var","$@" [[ ]]for conditionals, not[ ]- Derive paths from script location:
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - Find real binaries by walking PATH, skipping own directory
execfor final command — don't fork unnecessarily- Errors to stderr:
echo "Error: ..." >&2 - Graceful fallback when credential server isn't running (run unauthenticated)
- Python 3, stdlib only — no external dependencies
- Imports: stdlib grouped alphabetically, one per line
- Type hints in docstrings, not annotations
- ANSI escape codes via named constants (
DIM,BOLD,GREEN, etc.) - Catch specific exceptions, never bare
except: - Signal handlers for cleanup (SIGINT, SIGTERM)
- Concise over verbose. Simple over clever.
- LF line endings only.
- No em dashes (—) in code or comments. Use regular dashes or reword.
JSON over Unix socket (.credential-server.sock). One request-response per connection.
Request: {"type": "gh"}\n
Request: {"type": "aws", "profile": "my-profile"}\n
Request: {"type": "az"}\n
Request: {"type": "ssh"}\n
Request: {"type": "docker"}\n
Request: {"type": "netrc", "machine": "pypi.fury.io"}\n
Request: {"type": "open", "url": "https://..."}\n
Request: {"type": "aws", "profile": "p", "origin": "remote", "host": "...", "cmd": "...", "cwd": "..."}\n
Request: {"type": "open", "url": "https://...", "origin": "remote", "host": "...", "cmd": "...", "cwd": "..."}\n
Response: {"ok": true, "token": "gho_..."}\n
Response: {"ok": true, "access_key_id": "...", "secret_access_key": "...", "session_token": "..."}\n
Response: {"ok": false}\n
Approval is per (safehouse_pid, cred_key). Cred key: "gh", "aws:<profile>", "az", "ssh", "docker", or "netrc:<machine>".
Approving one credential doesn't approve others.
Single-screen approval form: hotkeys toggle each row, Enter confirms (defaults to once). Scope o=once/r=reads/p=pattern/a=all; duration for r/p/a 1/5/s (default 5min); g cycles pattern granularity; t toggles include-sensitive (default on when the command is itself sensitive); d/Esc=deny. Only one approval active per context.
By default, git/gh reads and non-protected-branch pushes (not main/master) are auto-approved.
Disable with --no-auto-git-reads or --no-auto-git-push.
Repos in main-push-repos under [auto-approve] in config.toml (as owner/repo, matched against the origin remote) also get main/master pushes auto-approved.
Profiles in aws-profiles under [auto-approve] are auto-approved; entries are glob patterns (*-read).
Docker reads (docker ps, docker images, etc.) are auto-approved by default.
Disable with --no-auto-docker-reads.
Netrc credential requests (for uv private indexes) are auto-approved by default.
Disable with --no-auto-netrc.
A remote host can request AWS credentials or a URL open by SSH-forwarding
.credential-server.sock to itself (ssh -R) and running the credential-aware
wrappers remote/aws / remote/open-url there. Over the forwarded socket the
server's process introspection only sees the local ssh client, so the wrapper
self-reports its context with extra request fields:
origin: "remote"switches the server to remote mode.hostscopes approvals to a synthetic(remote:<host>, cred_key)key (sanitized to[A-Za-z0-9._-], capped at 64 chars).cmd/cwdare shown in the approval prompt and audit log;cmddrives read-only / sensitive classification.
Remote mode serves aws and open only. Remote approvals are not persisted to
.approvals.toml; an r/p/a "session" approval lasts until the server
restarts. The approval form UX is otherwise identical to local.
Remote open is prompted per URL ([Enter] open / [d] deny) rather than
auto-approved like a sandbox open — any process on the remote host can reach
the forwarded socket. If the URL carries a loopback redirect_uri
(http://127.0.0.1:<port>/..., i.e. an OAuth authorize URL), the server also
runs ssh -O forward -L <port>:127.0.0.1:<port> <host> so the browser's
callback reaches the listener on the remote host, and cancels it after
LOOPBACK_TUNNEL_TTL. That needs the ssh ControlMaster for <host> to be up;
otherwise the manual ssh -L command is printed.
- Add fetch function in
credential-server(e.g.fetch_xyz_token()) - Handle new type in
handle_request()and_fetch_creds() - Create
bin/xyzwrapper followingbin/ghpattern - Mount needed read-only config in
safe.sh(--add-dirs-ro=...)
Note: az is approval-only (no credential injection) — it reads tokens from ~/.azure directly.
ssh is approval-only — it gates access to SSH_AUTH_SOCK (the SSH agent socket).
The wrapper strips SSH_AUTH_SOCK by default and restores it only if approved.
docker is approval-only with socket proxy -- the real Docker socket (/var/run/docker.sock)
is never mounted in the sandbox. The credential server creates a proxy socket
(.docker-proxy.sock) and only forwards connections from approved processes.
This is a hard security boundary, not just a PATH-based gate.
- Credentials NEVER live in the sandbox. Injected per-process via env vars scoped to the single CLI invocation.
- Credential server runs OUTSIDE sandbox with full host access.
bin/wrappers run INSIDE sandbox with restricted filesystem.~/.config/ghand~/.aws/configmounted read-only (config only, no secrets).- Keychain, SSO cache, and credential files NOT accessible from sandbox.
awswrapper strips--profileto avoid SSO lookup in sandbox; extracts region/output from config and injects asAWS_DEFAULT_REGION/AWS_DEFAULT_OUTPUT.- Remote mode: the server can't introspect a remote requester's process, so it
trusts the wrapper's self-reported
cmd/cwd/host. These are advisory (prompt display, audit log, read/sensitive classification) — they never narrow the returned credentials. Returned STS creds are full creds for the profile, so a remoter/a-mode approval means "this host may do anything with this profile until it expires". The real boundary is who can reach the forwarded socket. Remoteopentakes the same trust: the self-reported host is what the loopbackssh -O forwardtargets, so it is shown in the prompt and only acted on after approval.
Agent Safehouse — macOS kernel-level sandbox,
deny-by-default filesystem. safe.sh grants:
- Read-write:
~/Code/henrist/agentic-coding,~/.claude, plus cwd project dir if in allowlist - Read-only:
~/.gitignore,~/.config/gh,~/.aws/config - PATH: prepends
bin/for credential wrappers
Modify safe.sh to add new directories or integrations.
Dump the active sandbox policy to stdout (non-interactive):
SAFEHOUSE_POLICY=1 ./safe.sh trueThe policy is a macOS sandbox profile (Seatbelt .sb format) showing
all allow/deny rules. Use this to verify what the sandbox permits.
When something fails inside the sandbox, check macOS unified log for Seatbelt violations:
/usr/bin/log show --last 15m --predicate 'process == "sandboxd"' --info --debug 2>&1 | grep -i "deny\|violat"Each violation shows the process, operation, and target. Fix by adding
allow rules to the appropriate profile in profiles/.