didericis 20cfafcc4d
lint / lint (push) Successful in 1m33s
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 21s
feat: forward agent style via native CLI config and terminal title
Replace prompt-injection for display identity with native UI wiring:
- Claude: writes a statusline shell script + custom theme JSON, wired up
  via settings.json so label/color show in the status bar and theme
- Codex: writes [tui] block into codex-config.toml (status_line,
  terminal_title, dark-ansi theme)
- Both backends set the terminal title via ANSI OSC 0 escape before
  exec-ing the agent when a label is present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 00:00:19 -04:00

bot-bottle logo

bot-bottle

test pylint pyright

Problem: Developer wants to run a coding agent without supervision, but they don't want a prompt injected or misbehaving agent wrecking their environment or exfiltrating sensitive data.

Solution: Ephemeral, per agent "bottles" the agent cannot modify that scan all traffic for data exfiltration and limit capabilities and egress to only what the agent needs.

Features

  • Per-bottle egress allowlist — TLS-bumped HTTP/HTTPS chokepoint with a per-manifest host allowlist and request-body DLP scanner; 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.
  • Composable bottles (extends:) — keep provider/runtime policy in one base bottle (e.g. claude.md) and overlay task bottles on top.
  • Parallel, isolated bottles — each bottle runs in its own backend-owned isolation boundary; bottles don't share state or talk to each other.
  • Provider templates (Claude, Codex)Dockerfile.claude / Dockerfile.codex, or a bottle-supplied Dockerfile. Claude auth via long-lived OAuth token; Codex via opt-in host device-auth forwarding.
  • 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.
  • Smolmachines backend (macOS default) — 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 smolvm via BOT_BOTTLE_BACKEND=docker or --backend=docker.

Architecture

On the default 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.

Quickstart

Requires Docker on the host for the sidecar bundle, smolvm on macOS for the default backend, and 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 smolvm is not installed.

./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
        token_ref: BOT_BOTTLE_GITEA_TOKEN
---

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.

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.

S
Description
Lightweight, self-hosted sandbox for AI coding agents that protects against prompt-injected or misbehaving agents: all egress traffic is TLS-inspected and secret-scanned, and credentials are injected at the proxy so the agent never sees them. No third-party platform in the loop, no trust required.
Readme Apache-2.0 30 MiB
Languages
Python 99%
Shell 0.6%
Dockerfile 0.4%