Files
bot-bottle/README.md
didericis bd663196dc
test / unit (pull_request) Successful in 36s
test / integration (pull_request) Successful in 17s
docs: reposition README around provider-neutral secure substrate
Lead with the agnostic + security story instead of the single-user
security framing. New hero positions bot-bottle as a neutral control
plane that runs any agent (Claude, Codex, or a drop-in contrib plugin)
inside an isolation boundary the agent can't touch.

Restructure Features into three pillars — neutral substrate, isolation
boundary, host-matched isolation — promoting provider-agnosticism (PRD
0053 user plugins) from a buried bullet to a headline. No capability
claims changed; per-provider auth/image detail preserved as a note
linking to Manifest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcU7nerbg8cVj9R4EkpfLJ
2026-06-24 01:21:05 -04:00

13 KiB

bot-bottle logo

bot-bottle

test pylint pyright

Run any coding agent like it might be compromised — and lose nothing when it is.

bot-bottle is a provider-neutral, security-first substrate for autonomous agents. Bring Claude Code, Codex, or your own harness; each one runs in an ephemeral, per-agent "bottle" it cannot modify, where every byte of egress is scanned for exfiltration and capabilities are narrowed to exactly what the task declares.

Problem: You want to let a coding agent run unsupervised, but a prompt-injected or misbehaving agent — or a poisoned repo, MCP server, or skill — can wreck your environment or exfiltrate your secrets. Locking yourself to one vendor's cloud doesn't fix that; it just moves the blast radius.

Solution: A neutral control plane that runs whatever agent you choose inside an isolation boundary the agent can't touch: TLS-bumped egress allowlisting, outbound/inbound DLP, gitleaks-gated pushes, and host secrets the agent never sees. Swap the agent; keep the guarantees.

Why bot-bottle

A neutral substrate — bring your own agent

  • Provider-agnostic by design — Claude and Codex ship built in; any other agent (Gemini, Aider, a local-model wrapper) is a drop-in plugin at ~/.bot-bottle/contrib/<name>/ — no fork, no PR against this repo. The manifest accepts any provider template, and the isolation, egress, and git guarantees are identical across all of them.
  • One control plane, every harness — the same bottle, egress policy, and supervise flow wrap whichever agent you run, so switching or mixing providers doesn't change your security posture.
  • Composable bottles (extends:) — keep provider/runtime policy in one base bottle (e.g. claude.md) and overlay task bottles on top.

An isolation boundary the agent can't touch

  • Per-bottle egress allowlist — TLS-bumped HTTP/HTTPS chokepoint with a per-manifest host allowlist; per-route path/method/header matches filtering; outbound DLP scanning for known tokens and secrets, inbound DLP scanning for prompt-injection attempts; DoH and arbitrary hosts blocked by default.
  • Tokens the agent never sees — host secrets live in a sidecar; the agent dials http://sidecar:9099/<path> and the proxy strips inbound Authorization and injects the real token before forwarding. printenv in the agent shows proxy URLs only.
  • Gitleaks-scanned push (git-gate)bottle.git remotes route through a per-bottle git daemon that gitleaks-scans incoming refs pre-receive and forwards clean refs upstream over SSH. The agent never holds the upstream credential.
  • Manifest-scoped skills + secrets — each bottle declares its skills, env, git identity, remotes, and egress routes; unknown keys die at load.
  • Trust boundary at $HOME — bottles (credentials, egress, remotes) live only under ~/.bot-bottle/bottles/. Repos may ship agents but not bottles, so a cloned repo can't redirect an env var to an attacker host.

Isolation that matches your host

  • Parallel, isolated bottles — each bottle runs in its own backend-owned isolation boundary; bottles don't share state or talk to each other.
  • gVisor auto-detect — on Linux hosts where runsc is registered with Docker, every bottle launches under it for a userspace syscall barrier; no manifest config required.
  • Apple Container backend (macOS default when available) — runs the agent and sidecar bundle with Apple's container CLI, using a host-only agent network plus a separate sidecar egress network.
  • Smolmachines backend — runs the agent in a libkrun micro-VM while the sidecar bundle stays in Docker. TSI and smolmachines DNS filtering close the raw DNS exfiltration gap that exists in the legacy Docker backend.
  • Legacy Docker backend — still available for examples, CI, and hosts without Apple Container via BOT_BOTTLE_BACKEND=docker or --backend=docker.

Per-provider auth (Claude long-lived OAuth token; Codex opt-in host device-auth forwarding) and per-provider images (Dockerfile.claude / Dockerfile.codex, or a bottle-supplied Dockerfile) are configured on the bottle — see Manifest.

Architecture

On the default macOS Apple Container backend, a bottle is an agent container on a host-only internal network plus a sidecar bundle attached to both that internal network and a NAT egress network. The agent gets HTTP(S)_PROXY and CA bundle env vars pointing at the sidecar's internal-network IP, so HTTP/HTTPS traffic flows through the sidecar instead of direct egress. bottle.git / git-gate is intentionally deferred on this backend until a safe Apple Container key-delivery path exists.

On the smolmachines backend, a bottle is an agent micro-VM plus a Docker sidecar bundle for egress, git-gate, and supervise. The VM reaches the sidecars through a per-bottle loopback alias allowed by TSI; smolmachines handles DNS filtering below the guest OS.

On the legacy Docker backend, the same logical bottle is two containers per agent: an agent container and a sidecars container. They share a per-agent Docker --internal network; the agent has no default route off-box.

The Docker topology looks like this:

                            host  ( ./cli.py )
                                  │
                          starts  │  stops
                                  ▼
   ┌─────────────────────────── bottle ──────────────────────────────────┐
   │                                                                     │
   │   ┌──────────────────┐                   ┌──────────────────────┐   │
   │   │ agent image      │   HTTP(S) proxy   │ egress image         │   │
   │   │ (claude-code,    │ ─────────────────►│ (mitmproxy; TLS bump │   │  HTTPS to
   │   │  codex, etc)     │                   │  DLP scan, path      │───┼──►  allowlisted
   │   │                  │                   │  matching, auth      │   │     hosts
   │   │ environ: proxy   │                   │  injection)          │   │
   │   │ URLs only, no    │                   └──────────────────────┘   │
   │   │ real tokens      │                                              │
   │   │                  │    git proxy     ┌────────────────┐          │  SSH push/fetch
   │   │                  │ ────────────────►│ git-gate image │──────────┼──►  to bottle.git
   │   │                  │                  │ (gitleaks +    │          │      upstreams
   │   └──────────────────┘                  │  git daemon)   │          │     (direct — not
   │                                         └────────────────┘          │      via egress)
   │                                                                     │
   │   agent on internal network (no default route); egress and          │
   │   git-gate straddle internal + egress networks.                     │
   │   egress is the single HTTP/HTTPS chokepoint — all agent HTTP/HTTPS │
   │   traffic flows through it. git-gate's SSH egress is direct         │
   │   because egress is HTTP-only.                                      │
   └─────────────────────────────────────────────────────────────────────┘

When the agent exits, cli.py tears down every sidecar and both networks; nothing about a bottle persists between runs.

Install

Install the CLI with the bootstrap script:

curl -fsSL https://gitea.dideric.is/didericis/bot-bottle/raw/branch/main/install.sh | sh

The script checks Python 3.11+, checks Docker daemon reachability, creates the ~/.bot-bottle/ config directories, installs the Python package with pipx when available or pip --user otherwise, then runs:

bot-bottle doctor

Python-native installers can use the package metadata directly:

pipx install git+https://gitea.dideric.is/didericis/bot-bottle.git
uv tool install git+https://gitea.dideric.is/didericis/bot-bottle.git

Quickstart

On compatible macOS hosts, the default backend requires Apple's container CLI and does not require Docker. The smolmachines backend requires Docker on the host for the sidecar bundle plus smolvm. The legacy Docker backend requires Docker. Claude bottles also need a long-lived Claude Code OAuth token (claude setup-token) exported as BOT_BOTTLE_CLAUDE_OAUTH_TOKEN.

Use BOT_BOTTLE_BACKEND=docker ./cli.py start <agent> on hosts where Apple Container is not installed and Docker is the desired backend.

./cli.py start <agent>   # builds the image on first run, drops you into claude

Manifest

Bottles and agents are Markdown files with YAML frontmatter under ~/.bot-bottle/. The Markdown body is the system prompt. Bottles live in ~/.bot-bottle/bottles/; agents may also be shipped by a repo at <repo>/.bot-bottle/agents/<name>.md.

Bottle (~/.bot-bottle/bottles/gitea-dev.md):

---
extends: claude          # inherit the Claude provider boundary

env:
  GIT_AUTHOR_NAME: didericis

git:
  user:
    name: "Eric Bauerfeld"
    email: "eric+claude@dideric.is"
  remotes:
    gitea.dideric.is:
      Name: bot-bottle
      Upstream: ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git
      IdentityFile: /Users/didericis/.ssh/id_ed25519_gitea
      KnownHostKey: ssh-ed25519 AAAA...

egress:
  routes:
    - host: gitea.dideric.is
      auth:
        scheme: token        # Bearer | token
        token_ref: BOT_BOTTLE_GITEA_TOKEN
      matches:               # optional — restrict to specific paths/methods/headers
        - paths:
            - {type: prefix, value: /api/v1/}
          methods: [GET, POST, PATCH, DELETE]
      dlp:                   # optional — per-route detector overrides (default: all on)
        outbound_detectors: [token_patterns, known_secrets]
        inbound_detectors: false   # disable response scanning for this host
---

The `gitea-dev` bottle. Provider auth via the inherited Claude route;
gitea over SSH for push, token over HTTPS for the API.

Agent (~/.bot-bottle/agents/gitea-helper.md):

---
bottle: gitea-dev
skills:
  - init-prd
---

You help maintain Gitea-hosted projects.

Egress route fields:

Field Required Description
host yes Hostname to allowlist. One entry per host.
role no Reserved for future use. The key is recognised but any value is currently rejected at load. Provider auth routes (e.g. Claude's api.anthropic.com) are injected automatically from agent_provider.auth_token, not via role.
auth.scheme when auth present Bearer or token. Injected by the proxy; the agent never sees the value.
auth.token_ref when auth present Env-var name holding the secret on the host.
matches no Array of {paths, methods, headers} filters. A request must match at least one entry (if any are given) to be forwarded.
matches[].paths no Array of {type, value}. type is prefix (default), exact, or regex.
matches[].methods no Array of HTTP method strings, e.g. [GET, POST].
matches[].headers no Array of {name, value, type}. type is exact (default) or regex.
dlp no Per-route DLP overrides. Omit to use defaults (all detectors on).
dlp.outbound_detectors no false disables outbound scanning; list restricts to named detectors (token_patterns, known_secrets).
dlp.inbound_detectors no false disables inbound scanning; list restricts to named detectors (naive_injection_detection).
git.fetch no true permits smart HTTP clone/fetch (git-upload-pack) for this host. Push (git-receive-pack) remains blocked.

More examples in examples/. Full design lives under docs/prds/; the trust-boundary rationale is in docs/prds/0011-per-file-md-manifest.md.

Trademarks

bot-bottle is an independent project and is not affiliated with, endorsed by, or sponsored by Anthropic, PBC. "Claude" and "Claude Code" are trademarks of Anthropic, PBC; the project name uses "claude" descriptively to indicate that the tool runs Claude Code inside a sandbox.

License

Copyright 2026 Eric Bauerfeld. Licensed under the Apache License, Version 2.0. See LICENSE for the full text.