leash

agent
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Warn
  • network request — Outbound network request in dashboard/src/docker.ts
  • process.env — Environment variable access in dashboard/src/logs.ts
  • process.env — Environment variable access in dashboard/src/policy.ts
  • process.env — Environment variable access in dashboard/src/server.ts
  • process.env — Environment variable access in dashboard/src/skills.ts
  • network request — Outbound network request in dashboard/src/skills.ts
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Sandbox to run AI coding agents (Claude Code, Codex, Gemini, Grok) with full TLS inspection, domain allowlist enforcement, and Falco syscall monitoring

README.md
Leash Logo

Leash

A sandbox for AI coding agents: proxied traffic is decrypted and logged, and egress runs against an enforced allowlist.

License: MIT
Platform
Stack
Tests


Why Leash exists

In early 2026 an incident with the Grok CLI made a latent risk concrete. During a normal coding session the agent read local credential files and sent data to an undisclosed endpoint, while appearing to work normally.

Nothing about that is specific to Grok. Claude Code, Codex, and Gemini CLI all run as your user. They can read ~/.ssh, .env files, and ~/.aws credentials, and they can open HTTPS connections to any destination. You get no visibility into what they actually send.

Agent skills and plugins are a newer version of the same problem. They are files an agent reads and follows, so a malicious one can inject instructions, exfiltrate data, or escalate privileges. Until you scan it, you install it blind.

Leash addresses both:

  • An isolated environment where agents have no direct route to the internet
  • TLS decryption of every intercepted HTTPS request, so you see the body and not just the destination
  • An enforced domain allowlist, with everything outside it blocked and logged
  • Syscall-level monitoring via Falco, covering credential reads, writes outside the workspace, and unexpected processes
  • A web dashboard for watching all of that live, managing the allowlist, and controlling the sandbox
  • A security scan for any skill or plugin before it gets installed

Decryption, logging, and allowlist enforcement apply to traffic that actually goes through the proxy. Hosts listed in ignore_hosts are tunneled un-intercepted, so their request bodies are never decrypted and never reach the allowlist check or the flow log; see Adjusting the allowlist. Egress isolation and Falco monitoring still cover those hosts, since neither depends on the proxy.


Architecture

macOS host (Apple Silicon or Intel)
└── Lima VM  "agent-lab"  (Ubuntu 24.04, Virtualization.framework)
    ├── Falco  ─── modern eBPF on VM kernel ──▶ logs/falco.jsonl
    └── Docker Compose
        ├── network "jail"   (internal: true, no direct egress)
        ├── network "egress" (bridge; proxy, dashboard, skillspector reach internet)
        │
        ├── proxy   mitmproxy 11              [jail + egress]
        │           ├── full TLS decrypt (CA injected into agents)
        │           ├── domain allowlist enforcement → 403 + blocked.jsonl
        │           └── request body logging → flows.jsonl
        │
        ├── agents  node:22                   [jail only]
        │           ├── Claude Code
        │           ├── OpenAI Codex CLI
        │           ├── Gemini CLI
        │           └── Grok CLI
        │
        ├── dashboard  bun + hono + react     [egress only]
        │           ├── live flows/blocked/falco viewer (WebSocket)
        │           ├── policy editor + container control (docker.sock)
        │           └── Skills tab → calls skillspector, writes named volumes
        │
        └── skillspector  python + fastapi    [egress only]
                    └── scans a pasted skill/plugin URL before install

Traffic flow: agent → proxy (decrypt + enforce) → internet

The agents container has no direct egress at all, because an internal: true Docker network has no gateway. dashboard and skillspector sit outside the jail and reach the internet over egress; they are operator tooling, not something the agent drives.


What gets logged

Log file Contents
logs/flows.jsonl Every decrypted request: host, method, path, status, body hash, body preview (512 bytes)
logs/blocked.jsonl Every blocked attempt: timestamp, host, method
logs/falco.jsonl Syscall alerts: credential reads, bypass attempts, unexpected processes

Example of catching an exfiltration attempt:

// logs/blocked.jsonl: agent tried to reach an unknown endpoint
{"ts": 1783969672, "event": "blocked", "host": "telemetry.unknown-vendor.io", "method": "CONNECT", "path": "/"}

// logs/flows.jsonl: body of an allowed request, fully decrypted
{"host": "api.anthropic.com", "method": "POST", "path": "/v1/messages",
 "req_size": 54564, "req_body_preview": "{\"model\":\"claude-opus-4\",\"messages\":[...]}"}

// logs/falco.jsonl: agent read your SSH key
{"rule": "Agent reads sensitive credentials", "priority": "Critical",
 "output": "proc=node file=/home/agent/.ssh/id_rsa container=agents"}

Quick start

Prerequisites: macOS (Apple Silicon or Intel), Lima (brew install lima), Docker Desktop (for local image builds only).

# 1. Clone
git clone https://github.com/thiagolmoraes/leash
cd leash

# 2. Add your API keys
cp .env.example .env
# edit .env with real keys (and set MITMWEB_PASSWORD to protect the :8081 UI)

# 3. Bring everything up
make

What bare make does depends on whether the VM exists yet. On a first run it creates the Lima VM (a one-time ~500MB Ubuntu 24.04 download), builds the images, and starts the stack. If the VM is already there, it shows an interactive menu for opening a shell, starting the stack, opening the proxy UI, or stopping the VM.

The individual steps are still available: make vm-up for the VM only, make up to build and start the stack.


Usage

make claude        # run Claude Code inside the sandbox
make codex         # run OpenAI Codex CLI
make gemini        # run Gemini CLI
make grok          # run Grok CLI
make shell         # interactive bash as non-root agent user

make ui            # open mitmweb at http://localhost:8081 (live TLS flows)
make dashboard     # open the Leash dashboard at http://localhost:8082
make logs          # tail blocked.jsonl + falco.jsonl
make logs-flows    # tail all decrypted flows
make logs-blocked  # tail blocked attempts only
make logs-falco    # tail Falco syscall alerts

make test          # run full security test suite (19 tests)
make restart-proxy # reload after editing proxy/policy.yaml
make down          # stop containers
make vm-down       # stop Lima VM

Adjusting the allowlist

Edit proxy/policy.yaml. Switching to mode: observe lets allowlist misses through instead of returning a 403, which is how you discover what domains a newly added CLI legitimately needs. It changes allowlist enforcement only: requests are still decrypted and logged, the agents container still has no direct egress, and Falco keeps monitoring syscalls.

mode: enforce   # enforce | observe

allow:
  - api.anthropic.com
  - platform.claude.com
  - api.openai.com
  - generativelanguage.googleapis.com
  - api.x.ai
  # add domains here

# Optional: hosts to pass through un-intercepted (see "TLS cert pinning" below)
# ignore_hosts:
#   - pinned-api\.example\.com

After editing:

make restart-proxy   # no rebuild needed, policy is volume-mounted

The dashboard's Policy tab does the same thing without hand-editing YAML, but it only manages allow and mode. Setting ignore_hosts is YAML-only for now.

ignore_hosts is a real hole in coverage, so use it deliberately. Listed hosts are tunneled straight through: mitmproxy never decrypts them, the gatekeeper never checks them against the allowlist, and nothing about them lands in flows.jsonl. You keep egress isolation and Falco for those hosts, but you lose request visibility entirely. Unlike allow and mode, it is read once at proxy startup, so a change only applies after make restart-proxy.


Dashboard

make dashboard   # opens http://localhost:8082

A web UI covering everything the CLI, mitmweb, and jq combination above does by hand.

Flows

A live table of every decrypted request and response (host, method, path, status, size), updated over a WebSocket. Clicking a row expands the body preview and its sha256.

Flows tab, showing every decrypted request the proxy has seen

Blocked

Requests the allowlist rejected before they left the sandbox. Each row has an "Allow this host" button that writes straight to the policy.

Blocked tab, showing allowlist rejections including an exfil canary domain

Falco

Syscall-level alerts for credential reads, escapes, and bypass attempts, color-coded and filterable by priority.

Falco tab, showing syscall alerts by priority

Policy

Edits the allowlist and toggles between enforce and observe without touching YAML. "Save & reload proxy" applies the change immediately.

Policy tab, showing the allowlist editor and enforce/observe toggle

Containers

Status plus start, stop, and restart for proxy, agents, falco, and skillspector. It reports running state, not network membership; see How the isolation claims are verifiable.

Skills

Paste a skill or plugin git URL, scan it with SkillSpector before installing, and pick the harness to install it for (Claude Code, Codex, Gemini, or Grok). More detail in Skills tab below.

Skills tab, showing a scan field and install-target picker

Security note: the dashboard container mounts /var/run/docker.sock, which is host-Docker-equivalent access. Anyone who can reach :8082 can control any container on the host, not only this project's, and the dashboard has no authentication of its own. Only expose port 8082 on a trusted network, and don't port-forward it beyond localhost.


Skills tab: scan before you install

Skills and plugins run with the same trust as the CLI itself, so a malicious one can read files, exfiltrate data, or hijack the agent's behavior. The Skills tab scans a pasted URL with SkillSpector, covering 68 vulnerability patterns across prompt injection, data exfiltration, privilege escalation, and others, before anything lands in the sandbox.

The scan never blocks. It reports a 0-100 risk score and a full findings breakdown, and you decide whether to install anyway. A high score produces a louder warning rather than a locked button.

Supported git hosts are github.com, gitlab.com, and bitbucket.org, matching the allowlist SkillSpector itself uses, plus an SSRF guard against private and internal IPs.

A pasted URL can point at a single skill (SKILL.md at the repo root) or a collection (multiple skills/<name>/SKILL.md subdirectories, as in anthropics/skills). Leash detects which it is and installs one skill or all of them accordingly.

Install targets, one per harness. All four amount to dropping a folder in place, so no CLI exec is involved:

Harness Installed to (inside agents) Status
Claude Code ~/.claude/skills/<name>/ stable
Codex CLI ~/.agents/skills/<name>/ stable
Gemini CLI ~/.gemini/extensions/<name>/ stable (manifest synthesized if the source doesn't ship one)
Grok CLI ~/.grok/skills/<name>/ experimental; xAI hasn't published the manifest format, so installs reuse the same layout as the others on a best-effort basis

v1 runs static analysis only (--no-llm), so no API key is required. LLM-assisted scanning is wired up behind SKILLSPECTOR_PROVIDER in .env and works with both Anthropic-compatible and OpenAI-compatible endpoints, but it is off by default.

Installed skill files stay inside Docker. Each harness's install directory is a named volume (skills-claude, skills-codex, skills-gemini, skills-grok) shared only between dashboard and agents, rather than a bind mount to the macOS filesystem, so installing a skill writes nothing to the host outside Docker's own storage.

That covers where the files live, not what they can do once an agent runs them. A skill executes with the agents container's mounts and permissions, which includes the ./workspace and ./logs bind mounts, and its network activity is subject to the same proxy and allowlist as everything else in the jail. A scan flags potential behavior rather than validating actual behavior, since v1 is static analysis; what a skill can really do at runtime comes down to the container's mounts, permissions, and network controls. The volume layout only keeps the install itself off the host filesystem.


Falco rules

Five detection rules ship by default (falco/rules.local.yaml):

Rule Priority Triggers on
Agent reads sensitive credentials Critical Read of ~/.ssh/*, ~/.aws/*, ~/.config/gcloud/*, *.pem, .env*
Agent direct outbound (proxy bypass) Critical TCP connect not destined for proxy
Agent spawns network tool Warning nc, ncat, ssh, scp, socat, nmap spawned
Agent writes outside workspace Warning File write outside /workspace and /tmp
Unexpected interactive shell Notice TTY shell spawned by unexpected parent

Honeypot files are pre-seeded in the agent home (~/.ssh/id_rsa, ~/.aws/credentials) to make credential-read detection reliable even if the agent never touches real keys.


Test suite

make test
── pre-flight ──
PASS  container proxy is running
PASS  container agents is running
PASS  container falco is running

── TLS interception ──
PASS  TLS interception: HTTPS reaches API (HTTP 401, not SSL error)
PASS  TLS interception: mitm CA in system trust store

── egress isolation ──
PASS  egress isolation: direct outbound blocked (no route)
PASS  egress isolation: direct DNS bypass blocked

── allowlist enforcement ──
PASS  allowlist: blocked domain returns 403/reset
PASS  allowlist: block logged to blocked.jsonl
PASS  allowlist: allowed domain passes

── flow logging ──
PASS  flow logging: request logged
PASS  flow logging: JSON has required fields

── Falco detection ──
PASS  Falco: credential read detected (rule: Agent reads sensitive credentials)
PASS  Falco: still running after tests

── filesystem isolation ──
PASS  Falco: write outside workspace triggered alert

── CLI availability ──
PASS  CLI installed: claude
PASS  CLI installed: codex
PASS  CLI installed: gemini
PASS  CLI installed: grok

Results: 19 passed  0 failed  0 skipped

How the isolation claims are verifiable

You don't have to take "agents can't reach the host" on faith from this README. Where to check each claim:

  • Network topology lives in the networks: section of docker-compose.yml. agents is declared on jail only, dashboard on egress only, and jail has no bridge to egress. There is no hidden config elsewhere.
  • Whether the isolation holds at runtime, and not just in config, is covered by make test-bypass and make test-block (see Test suite). Both run real commands inside the agents container and assert the network-level result, such as a refused connection or a 403.
  • The dashboard's docker.sock exposure is the one place a compromise would be severe. The MANAGED allowlist in dashboard/src/docker.ts and the network assignment for dashboard in docker-compose.yml are both plain text, neither obfuscated nor generated at runtime.

One current gap: the Containers tab shows each container's running state but not which Docker network it sits on. Verifying network isolation therefore means reading docker-compose.yml or running the test suite, since the UI doesn't surface it yet. Treat the compose file as the source of truth here, not the dashboard.


Known limitations

Issue Workaround
TLS cert pinning: a CLI pins its cert, so MitM breaks for that host Add the host to ignore_hosts, accepting that its traffic is then tunneled un-inspected and unfiltered. mode: observe does not help here, since it changes allowlist enforcement and leaves TLS interception on
OAuth login flows: browser-based auth is awkward inside a container Use API keys in .env instead of interactive login
Falco runs inside the Lima VM kernel (Linux 6.x), not on the macOS host directly Nothing to work around, since eBPF works on the VM kernel
Firecracker and KVM are unavailable on macOS (no hardware KVM on Apple Silicon or Intel) Lima with Virtualization.framework provides sufficient isolation
Intel Mac support: the Lima config lists an x86_64 image alongside arm64 and Lima auto-selects, but this is unverified on real Intel hardware Report back if you try it, since this needs real-world confirmation

License

MIT

Reviews (0)

No results found