{"id":51035639,"url":"https://github.com/okulik/glovebox","last_synced_at":"2026-06-22T05:32:23.501Z","repository":{"id":364858557,"uuid":"1269243760","full_name":"okulik/glovebox","owner":"okulik","description":"A Docker-based isolation harness for running AI coding agents","archived":false,"fork":false,"pushed_at":"2026-06-14T20:04:06.000Z","size":292,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T21:18:51.233Z","etag":null,"topics":["docker","llm-agents","llm-tools","sandboxing","squid-proxy"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/okulik.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-14T13:29:31.000Z","updated_at":"2026-06-14T20:04:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/okulik/glovebox","commit_stats":null,"previous_names":["okulik/glovebox"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/okulik/glovebox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okulik%2Fglovebox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okulik%2Fglovebox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okulik%2Fglovebox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okulik%2Fglovebox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/okulik","download_url":"https://codeload.github.com/okulik/glovebox/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okulik%2Fglovebox/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34636427,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","llm-agents","llm-tools","sandboxing","squid-proxy"],"created_at":"2026-06-22T05:32:21.725Z","updated_at":"2026-06-22T05:32:23.489Z","avatar_url":"https://github.com/okulik.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# glovebox\n\n\u003cimg width=\"500\" alt=\"glovebox\" src=\"https://github.com/user-attachments/assets/bd9ca342-32a6-4a62-b368-f4e3193c01c3\" /\u003e\n\nA Docker-based isolation harness for running AI coding agents.\nSupports seven agents - **Claude Code, Codex, OpenCode, Pi, Gemini CLI,\nAider, Hermes** - each launched with `gbx run \u003cagent\u003e`. The image ships\nonly a thin wrapper binary (`gbxa`, dispatched by the agent name it's\ninvoked as); the real agent binary is installed on first use into a\nbind-mounted state directory, so subsequent launches start instantly and\nOAuth state survives container recreation. The agent runs\nin a persistent container with a domain-allowlist HTTP proxy as its only\npath to the internet, while interactive permission prompts inside the\nagent provide the second layer of control.\n\nThe name is the lab apparatus: you reach in through sealed gloves to\nmanipulate something dangerous, but it can't reach out.\n\n## Contents\n\n- [Prerequisites](#prerequisites)\n- [Install](#install)\n- [Quickstart](#quickstart)\n- [Commands](#commands) - full CLI reference\n- [Configuration](#configuration) - auth, mounts, allowlist, state, uninstall\n- [How it works](#how-it-works) - security model, networks, dev-stack internals, env vars\n- [Development](#development) - building, testing\n- [Maintainers](#maintainers) - release process\n\n---\n\n## Prerequisites\n\nmacOS or Linux with a Docker-compatible runtime installed and running. Any\nof these work; `gbx` talks to the Docker Engine API directly via the moby\nSDK (the `docker build` shell-out for first-time image builds is the only\nremaining CLI dependency), so anything that exposes a Docker socket is fine:\n\n- [OrbStack](https://orbstack.dev) - recommended on macOS; what the test\n  suite is exercised against.\n- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (macOS or Linux).\n- [Colima](https://github.com/abiosoft/colima) (`brew install colima docker`).\n- [Rancher Desktop](https://rancherdesktop.io) with the `dockerd (moby)` engine.\n- Native [Docker Engine](https://docs.docker.com/engine/install/) on Linux\n  (rootful; the daemon socket at `/var/run/docker.sock`).\n\nThe agent container runs as your host user's UID/GID so bind-mounted files\nstay owned by you. On macOS the Docker file-sharing layer maps ownership\nautomatically; on native Linux the match is exact, derived from `id -u` /\n`id -g` at build and run time.\n\n## Install\n\n### Via Homebrew\n\n```bash\nbrew tap okulik/glovebox\nbrew trust --formula okulik/glovebox/glovebox    # brew 5.x; see below\nbrew install glovebox\n```\n\n`brew tap okulik/glovebox` resolves to the\n[`okulik/homebrew-glovebox`](https://github.com/okulik/homebrew-glovebox)\ntap repository (Homebrew inserts the `homebrew-` prefix automatically).\n`brew install glovebox` then resolves to the formula from that tap as long\nas no other tapped formula shares the name (homebrew-core has no\n`glovebox`). The `brew trust` step is Homebrew 5.x's opt-in for third-party\ntaps; without it `brew install` runs but doesn't link a `gbx` binary on\nPATH.\n\n### From source\n\nRequires Go. (The Homebrew path also builds from source, but pulls in Go\nautomatically as a build-time dependency, so you don't install it yourself.)\n\n```bash\ngit clone https://github.com/okulik/glovebox ~/dev/glovebox\ncd ~/dev/glovebox\nmake build      # compiles bin/gbx\n```\n\nPut `bin/` on your PATH so you can drop the `bin/` prefix:\n\n```bash\necho 'export PATH=\"$HOME/dev/glovebox/bin:$PATH\"' \u003e\u003e ~/.bashrc\n```\n\n## Quickstart\n\n```bash\ngbx new ~/projects/my-app         # bootstraps ~/.config/glovebox (seeds .env\n                                  # from the shipped template), builds the\n                                  # image on first run, creates the agent,\n                                  # sets it as default (~5 min on first run)\n$EDITOR ~/.config/glovebox/.env   # set the provider keys you have\ngbx run claude                    # interactive Claude Code session\n```\n\nAny of the bundled agents works the same way:\n\n```bash\ngbx run codex\ngbx run opencode\ngbx run pi\ngbx run gemini\ngbx run aider\ngbx run hermes\n```\n\nConfiguration and per-agent state live under `~/.config/glovebox/`.\n\n---\n\n## Commands\n\n`gbx` is structured as `gbx [global-flags] \u003ccommand\u003e [subcommand] [args]`.\nRun `gbx help` for an inline summary; this section is the reference.\n\n**Global flags**\n\n| Flag | Purpose |\n|---|---|\n| `-p`, `--pid \u003cid-or-prefix\u003e` | Override the default project for one invocation. Prefix-resolved; ambiguous prefixes are rejected. |\n| `--version` | Print the gbx version. |\n| `--help` | Print help for the current command. |\n\n### Projects\n\nA *project* is a host workspace directory mapped to a per-project agent\ncontainer. The project id (`pid`) is the first 12 hex chars of\n`sha1(realpath(workspace))`. State lives under\n`~/.config/glovebox/state/projects/\u003cpid\u003e/`. Most of the commands below\ntarget the default project (set by `gbx use`); pass an id, a prefix, or\nuse the global `-p` flag to target another.\n\n| Command                                                       | Purpose                                                                  |\n|---------------------------------------------------------------|--------------------------------------------------------------------------|\n| `gbx new \u003cpath\u003e`                                              | Register a workspace; create its agent; first project becomes the default. |\n| `gbx use \u003cid-or-prefix\u003e`                                      | Switch the default project pointer.                                      |\n| `gbx ls [-v] [--json]`                                        | List projects (`*` marks default). `-v` adds containers; `--json` emits structured output. |\n| `gbx rm \u003cid\u003e [--delete-state] [-y]`                           | Stop and remove a project's agent. State dir kept unless `--delete-state`. |\n| `gbx rm --all [--delete-state] [-y]`                          | Remove every registered project. Same state-dir rule as the single-pid form. |\n| `gbx start\\|stop\\|restart [\u003cid\u003e]`                             | Per-project agent lifecycle.                                             |\n| `gbx rebuild [\u003cid\u003e] [--all]`                                  | Rebuild `glovebox-agent:local` and recreate the agent.                   |\n| `gbx rebuild --controller`                                   | Rebuild `glovebox-stack-controller:local` from source and recreate it.   |\n| `gbx state-size [\u003cid\u003e]`                                       | Disk usage of one project plus the shared caches.                        |\n| `gbx mount \u003csubcommand\u003e`                                      | Per-project extra bind mounts - see below.                               |\n| `gbx plugin \u003csubcommand\u003e`                                     | Per-project Dockerfile plugins - see below.                              |\n\n#### `gbx mount` - extra bind mounts\n\nBy default the only host directory mounted into the agent is the workspace\n(`/workspace`). `mount` lets you attach additional host directories - a\nsibling library, a shared docs folder, a scratch directory. The set is\npersisted at `~/.config/glovebox/state/projects/\u003cpid\u003e/mounts.txt`, one\n`host:container:mode` per line.\n\n| Subcommand | Purpose |\n|---|---|\n| `add \u003chost\u003e[:\u003ccontainer\u003e][:rw\\|ro]` | Append a mount. Bare host → `/mnt/\u003cbasename\u003e:rw`. |\n| `rm \u003chost-or-container\u003e` | Drop a mount by either side. |\n| `ls` | Print the current set. |\n| `apply` | Force-recreate the agent so changes take effect. |\n\nExample:\n\n```bash\ngbx mount add ~/refs/design-docs:/mnt/docs:ro\ngbx mount add ~/code/shared-lib            # defaults to /mnt/shared-lib:rw\ngbx mount ls\n# /Users/you/refs/design-docs:/mnt/docs:ro\n# /Users/you/code/shared-lib:/mnt/shared-lib:rw\ngbx mount apply\ngbx run -- ls /mnt/docs /mnt/shared-lib\n```\n\nContainer paths claimed by the runtime (`/workspace`, `/home/gbx/.claude`,\n`/home/gbx/.npm`, …) are refused to avoid shadowing agent state. Host paths\nare symlink-resolved so the on-disk record matches what Docker actually\nmounts. Changes take effect on the next `gbx mount apply` (or the next\n`gbx rebuild` / `gbx new`).\n\n#### `gbx plugin` - custom image content\n\nThe agent image is shared across projects. To add project-specific tools or\npackages without forking the base Dockerfile, use plugins - Dockerfile\nfragments layered on top of the base image.\n\n```sh\ngbx plugin add            # opens $EDITOR with an instructional template\ngbx plugin ls             # list this project's plugins\ngbx plugin edit \u003cid\u003e      # edit a fragment\ngbx plugin rm \u003cid\u003e        # remove a fragment\ngbx rebuild               # apply: builds glovebox-agent-\u003cpid\u003e:local and recreates the container\n```\n\nEach fragment must start with a description line:\n\n```dockerfile\n# gbx:description: install httpie and ripgrep\nRUN uv tool install httpie\n```\n\nRules: no `FROM` line (it is generated for you) and no `ADD` (use `COPY` or\n`RUN curl`). The build runs as root; the container still runs as the `gbx`\nuser. A project with no plugins keeps running the shared base image.\n\nPlugin changes apply only on the next `gbx rebuild`: `add` and `edit` alone\nchange nothing at runtime, and removing the last plugin does NOT revert the\nproject to the base image on a plain `gbx start`/`restart` - the stale\n`glovebox-agent-\u003cpid\u003e:local` image is only dropped when you run `gbx rebuild`.\n\n### `gbx run` - work in a project's agent\n\n| Form | Purpose |\n|---|---|\n| `gbx run` | Bash shell inside the default project's agent. |\n| `gbx run -- \u003ccmd...\u003e` | One-shot command inside the agent. |\n| `gbx run \u003cagent\u003e [args...]` | Launch one of the bundled agents: `claude`, `codex`, `opencode`, `pi`, `gemini`, `aider`, `hermes`. The agent's own flags pass through unchanged. |\n\nExample:\n\n```bash\ngbx run                          # interactive bash shell in the agent\ngbx run -- npm test              # one-shot command; exits with its status code\ngbx run -- ls /workspace /mnt    # peek at mounts without opening a shell\ngbx run claude                   # launch the bundled Claude Code agent\ngbx run codex --help             # an agent's own flags pass straight through\ngbx -p 1a2b3c run -- pytest -q   # target a non-default project by pid prefix\n```\n\nThe command runs as the in-container user against `/workspace`, brings the\nshared stack up first if needed, and creates the project's agent container on\nfirst use. Without `-p`, it targets the active project that `gbx use` set.\n\n### `gbx update \u003cagent\u003e` - refresh an agent in place\n\nReinstalls a bundled agent at its latest published version inside the\ncontainer. Per-agent state in `state/\u003cagent\u003e/` is preserved.\n\nExample:\n\n```bash\ngbx update claude                # reinstall Claude Code at its latest version\ngbx update aider                 # any bundled agent: claude codex opencode pi gemini aider hermes\ngbx -p 1a2b3c update gemini      # update the agent in a specific project's container\n```\n\nThe resolved install command (npm / uv) is echoed to stderr before it runs,\nand like `gbx run` the target defaults to the active project unless `-p` is\ngiven.\n\n### `gbx logs [proxy|controller]` - tail a stack component\n\nStreams a singleton-stack component's logs to your terminal (follows live):\n\n| Target | Stream |\n|---|---|\n| `proxy` (default) | The shared egress-proxy (Squid) access log - every allowed/blocked CONNECT. |\n| `controller` | The `stack-controller` HTTP server's stdout/stderr - manifest applies, image pulls, reconcile and request logs. |\n\n```bash\ngbx logs                  # same as `gbx logs proxy`\ngbx logs controller       # watch the stack-controller while debugging `gbx stack apply`\n```\n\n`controller` is the place to look when a `gbx stack apply` is rejected or a\nservice won't come up: the controller logs the validation failure, pull\nerror, or healthcheck timeout that the apply rolled back on.\n\n### `gbx allow \u003cdomain\u003e` - extend the egress allowlist\n\nAppends a domain to `~/.config/glovebox/allowlist.txt` and sends `SIGHUP`\nto Squid so the entry takes effect immediately (no restart). Lines that\nstart with `.` match the domain and any subdomain.\n\nThe fastest path when something is blocked:\n\n```bash\ngbx logs proxy \u0026              # tail in a side terminal\n# reproduce the failure\ngbx allow some-host.example\n```\n\nA blocked CONNECT returns HTTP `451 Unavailable For Legal Reasons` with an\n`X-Glovebox-Egress: blocked; reason=domain-not-allowlisted; add-via='gbx allow \u003cdomain\u003e'`\nresponse header - two distinct signals that agents (and humans) can use to\ntell a sandbox block apart from an origin's own 4xx. The on-disk agent\ninstructions injected into each project's `CLAUDE.md` / `AGENTS.md` /\n`GEMINI.md` already include the convention so the agent reacts correctly.\n\n### `gbx stack` - per-project dev services\n\nA project can declare auxiliary services (Redis, Postgres, Neo4j, …) via\na stack manifest. The agent proposes; the operator approves and applies.\nApproved services come up on a per-project `internal: true` network\n(`glovebox-stack-\u003cpid\u003e`), which the agent joins on apply, so DNS names\nlike `redis:6379` resolve from inside the agent.\n\nAll stack subcommands except `ls` and `image-allow` target a project; select\nit with the global `-p \u003cid\u003e` flag (placed before the subcommand), the\n`GBX_PROJECT_ID` env var, or fall back to the active project set by `gbx use`.\n\n| Subcommand | Purpose |\n|---|---|\n| `apply [--dry-run] [-y]` | Apply the controller's stored proposed manifest. |\n| `diff` | Show live vs proposed manifest. |\n| `down` | Stop services; keep volumes. |\n| `destroy [-y]` | Stop + remove services and volumes. |\n| `status` | Show service health. |\n| `ls` | List projects that have stacks. |\n| `logs \u003csvc\u003e [--follow]` | Stream a service's logs. |\n| `image-allow \u003cregistry\u003e` | Append a registry to `docker/image-allowlist.txt`. |\n\nExample - approving and operating a stack the agent proposed:\n\n```bash\n# The agent ran `gbx-stack propose \u003cfile\u003e`, submitting the manifest to the\n# controller.  No workspace file is written.  Review and apply from the host:\n\ngbx stack diff                                  # review proposed vs live\ngbx -p 1a2b3c4d5e6f stack apply -y              # validate, pull, start, attach agent\n\ngbx -p 1a2b3c4d5e6f stack status               # service health\ngbx -p 1a2b3c4d5e6f stack logs redis --follow\ngbx stack ls                                    # every project that has a stack\n\n# Tear down when finished:\ngbx -p 1a2b3c4d5e6f stack down                 # stop services, keep volumes\ngbx -p 1a2b3c4d5e6f stack destroy -y           # also remove named volumes\n```\n\n`apply` and `destroy` prompt `[y/N]` before acting; pass `-y` to skip the\nprompt (required when stdin isn't a terminal). `gbx stack apply --dry-run`\nprints the manifest that would be sent without contacting the controller.\nWhen no project resolves (no `-p`, no `GBX_PROJECT_ID`, no active project)\nthese commands error rather than guessing.\n\nThe agent observes and operates the live stack via a separate in-container\nCLI, `gbx-stack` - see [Dev stack details](#dev-stack-details).\n\n---\n\n## Configuration\n\n### Auth: API keys vs login\n\nEach agent looks at a different set of provider keys. The simplest path is\nto drop keys into `~/.config/glovebox/.env` - they are passed through to the\ncontainer's environment, where the agents pick them up:\n\n```\nANTHROPIC_API_KEY=    # Claude, Aider, OpenCode, Hermes, Pi\nOPENAI_API_KEY=       # Codex, Aider, OpenCode, Hermes, Pi\nOPENROUTER_API_KEY=   # Aider, OpenCode, Hermes, Pi\nGOOGLE_API_KEY=       # Gemini CLI, Aider, Pi (default provider)\nDEEPSEEK_API_KEY=     # Aider, Pi\nGROQ_API_KEY=         # Aider, Pi\nMISTRAL_API_KEY=      # Aider, Pi\n```\n\nFor agents that prefer OAuth (Claude Code, Codex, OpenCode, Gemini CLI,\nHermes), use the agent's native login command from within the container.\nCredentials land in `~/.\u003cagent\u003e/` which is bind-mounted to `state/\u003cagent\u003e/`,\nso they survive container recreation.\n\n```bash\ngbx run claude              # then run /login from inside Claude\ngbx run codex login\ngbx run opencode auth login\ngbx run gemini\ngbx run hermes login\n```\n\n`gbx run -- agent-auth` shows a status table for every agent (env / oauth /\nnone) - useful for confirming a setup is wired correctly.\n\n### State directories\n\nEverything under `~/.config/glovebox/state/` is bind-mounted into the agent\nand survives container restarts.\n\nPer-project state lives at `state/projects/\u003cpid\u003e/`:\n\n| Path | Purpose |\n|------|---------|\n| `claude/` | Claude Code config, login, projects, history |\n| `codex/` | Codex config and OAuth credentials |\n| `opencode/` | OpenCode config and provider login state |\n| `pi/` | Pi config (sessions, skills, extensions) |\n| `gemini/` | Gemini CLI config and auth state |\n| `aider/` | Aider config, cache, `.aider.*` history files |\n| `hermes/` | Hermes config, sessions, logs, skills, .env |\n| `workspace-path` | Pointer to the host workspace |\n| `mounts.txt` | Extra bind mounts added with `gbx mount` |\n\nShared caches live at `state/shared/`:\n\n| Path | Purpose |\n|------|---------|\n| `npm/` | npm cache |\n| `uv-tools/` | uv tool environments (aider) |\n| `bin/` | uv-managed binaries (aider) |\n| `cache/` | pip / uv build caches |\n| `shell-history/` | bash history (debug aid) |\n\n`gbx rebuild` recreates a project's agent container from a fresh\nimage without touching this state. For a project with plugins it also\nbuilds or refreshes the derived `glovebox-agent-\u003cpid\u003e:local` image (base\nimage + the project's fragments); for a project with none it drops any\nstale derived image so the container reverts to the base image. That\nrevert happens only on `gbx rebuild` - removing the last plugin does not\nchange a running or restarted container until you rebuild. `gbx rm \u003cid\u003e`\nremoves only the container by default; pass `--delete-state` to also wipe\nthe per-project directory.\n\n`gbx rebuild --controller` is the control-plane equivalent: it\nrebuilds the singleton `glovebox-stack-controller:local` image from\ncurrent source and recreates its container. `gbx up` won't do this on\nits own - it skips the build once the image exists - so reach for\n`--controller` after changing controller code (e.g. adding an API\nroute). Project services and agents keep running; only the controller\nAPI blips during the ~30s rebuild, and `state/controller/` is\npreserved.\n\n### Allowlists at a glance\n\nTwo distinct files with two distinct purposes:\n\n| File | Used by | Gates |\n|---|---|---|\n| `~/.config/glovebox/allowlist.txt` | `egress-proxy` (Squid) | Outbound HTTPS from the agent (`gbx allow`) |\n| `docker/image-allowlist.txt` | `stack-controller` | Image registries for `gbx stack apply` pulls |\n\nThe egress allowlist hot-reloads on `SIGHUP`. The image allowlist requires\na controller restart (`docker restart glovebox-stack-controller`, or the\nnext time `gbx up` brings the stack up). Lines beginning with `.` in the\negress allowlist match a domain and any subdomain; both files ignore `#`\ncomments and blank lines.\n\n### Singleton stack control\n\n`gbx up` (idempotent) ensures the shared three-container stack -\negress-proxy, socket-proxy, stack-controller - is healthy. Every command\nthat needs it (`gbx new`, `gbx run`, `gbx start`, …) calls it\nautomatically; you only invoke it directly for operator sanity checks or\nafter a manual `docker stop` of the stack containers.\n\n### Uninstall\n\n```bash\ngbx rm \u003cid\u003e            # one project's agent + state\nmake uninstall         # hard reset: every glovebox container, the\n                       # agent image, and ~/.config/glovebox\n                       # (prompts unless FORCE=1)\n```\n\nIf you installed via Homebrew (no Makefile available), run the equivalent\ncommands by hand:\n\n```bash\ndocker container ls -a --filter name='^glovebox-' --format '{{.Names}}' | xargs -r docker rm -f\ndocker network  ls    --format '{{.Name}}' | grep '^glovebox' | xargs -r -I {} docker network rm {}\ndocker volume   ls    --format '{{.Name}}' | grep '^glovebox' | xargs -r docker volume rm\ndocker image    rm glovebox-agent:local\nrm -rf ~/.config/glovebox\n```\n\n---\n\n## How it works\n\n### Security model\n\nThe container is the outer safety net. Interactive permission prompts\ninside the agent are the inner one. Specifically:\n\n1. The work container has **no default route to the internet** - it sits\n   on a Docker `internal: true` network whose only outside-facing member\n   is the proxy.\n2. The proxy permits only HTTPS CONNECT to domains in `allowlist.txt`.\n   Plain HTTP and non-HTTPS CONNECTs are denied.\n3. The container runs as **your host user's UID/GID** (derived from\n   `os.Getuid()` / `os.Getgid()` at image-build and container-create time)\n   with `cap_drop: [ALL]` and `no-new-privileges`. Files written to\n   `/workspace` appear owned by you on the host - on macOS via the Docker\n   file-sharing layer, on native Linux because the container UID matches\n   your host UID directly.\n4. The harness **does not pass `--dangerously-skip-permissions`**.\n   Approve tool calls in the usual agent prompts.\n\nRe-run the suite at any time to confirm these properties hold:\n\n```bash\nmake test\n```\n\n### Network topology\n\nFour bridge networks separate the components. Only one of them\n(`glovebox-egress`) can reach the internet; the other three are\n`internal: true`. The agent's only outbound paths are to the egress\nproxy (via `HTTPS_PROXY`) and to the stack-controller's `:7000` API.\n\n```\n   host (macOS / Linux)\n   ────────────────────\n     /var/run/docker.sock              127.0.0.1:7001\n              │                                │\n              │ RO bind                        │ host-only listener\n              ▼                                ▼\n\n   ┌─ glovebox-control   (internal) ─────────────────────────────────┐\n   │                                                                 │\n   │    socket-proxy   ◄── tcp/2375 ──   stack-controller            │\n   │                                                                 │\n   └─────────────────────────────────────────────────────────────────┘\n\n   ┌─ glovebox-internal  (internal) ─────────────────────────────────┐\n   │                                                                 │\n   │    stack-controller  ◄─ /api ─  agent  ─ HTTPS_PROXY ─► egress- │\n   │           :7000                                          proxy  │\n   │                                                                 │\n   └─────────────────────────────────────────────────────────────────┘\n\n   ┌─ glovebox-egress    (has internet) ─────────────────────────────┐\n   │                                                                 │\n   │    stack-controller    egress-proxy  ─► HTTPS (Squid allowlist) │\n   │           │                                                     │\n   │           └──► image registries (pulls at apply time)           │\n   │                                                                 │\n   └─────────────────────────────────────────────────────────────────┘\n\n   ┌─ glovebox-stack-\u003cpid\u003e   (internal, one per project) ────────────┐\n   │                                                                 │\n   │    agent  (attached on `gbx stack apply`)                       │\n   │       │                                                         │\n   │       ▼                                                         │\n   │    redis    neo4j    postgres    …    (services from manifest)  │\n   │                                                                 │\n   └─────────────────────────────────────────────────────────────────┘\n```\n\nMembership at a glance:\n\n| Container          | control | internal | egress | stack-`\u003cpid\u003e` |\n|--------------------|:-:|:-:|:-:|:-:|\n| `socket-proxy`     | ✓ |   |   |   |\n| `stack-controller` | ✓ | ✓ | ✓ |   |\n| `egress-proxy`     |   | ✓ | ✓ |   |\n| `agent`            |   | ✓ |   | ✓ (after apply) |\n| stack services     |   |   |   | ✓ |\n\nThe `socket-proxy` container runs\n[`tecnativa/docker-socket-proxy`](https://github.com/Tecnativa/docker-socket-proxy),\na small HTTP daemon that read-only-mounts the host's\n`/var/run/docker.sock` and re-exposes it as `tcp://socket-proxy:2375`,\nfiltered to a per-endpoint allowlist. Only `CONTAINERS`, `NETWORKS`,\n`VOLUMES`, `IMAGES`, `VERSION`, and `POST` are enabled; `EXEC`, `AUTH`,\n`SECRETS`, `SERVICES`, `SWARM`, `SYSTEM`, and `INFO` are off. It is the\ncontroller's only path to the Docker daemon - the controller never gets\nthe raw socket - so even a fully compromised controller can't shell\ninto containers, read secrets, or touch swarm state.\n\nKey properties this enforces:\n\n- The **agent** has no path to the Docker socket - `socket-proxy` lives\n  only on `glovebox-control`, which the agent never joins.\n- The **agent** has no direct internet route - the only egress path is\n  through `egress-proxy`'s HTTPS allowlist.\n- **Stack services** have no internet at all - `glovebox-stack-\u003cpid\u003e`\n  is `internal: true` and they're the only members along with the\n  attached agent.\n- The **stack-controller** is the only container straddling control,\n  internal, and egress. Image pulls happen on `glovebox-egress` from\n  the controller, never from the agent.\n\n### Multi-project layout\n\nGlovebox derives a 12-character project ID from the canonical workspace\npath (`sha1(realpath(path))[:12]`) and uses it to namespace the agent\ncontainer (`glovebox-agent-\u003cpid\u003e`), the stack network\n(`glovebox-stack-\u003cpid\u003e`), and the per-project state directory\n(`state/projects/\u003cpid\u003e/`).\n\n```bash\ngbx new ~/projects/app-a    # creates agent for A; sets as default\ngbx new ~/projects/app-b    # creates agent for B; A keeps running, default unchanged\ngbx use \u003cb-pid\u003e             # switch default to project B\ngbx run claude              # routes to project B (default)\ngbx -p \u003ca-pid\u003e run claude   # routes to project A without changing default\ngbx ls                      # lists projects, agents, stacks\ngbx rm \u003cid\u003e                 # stops + removes a project's agent and state\n```\n\nShared caches (`npm`, `uv-tools`, build caches, shell history) live under\n`state/shared/` and are bind-mounted into every project's agent.\n\n### Dev stack details\n\nThe dev-stack workflow:\n\n1. The agent writes a draft and runs `gbx-stack propose \u003cfile\u003e`, which\n   POSTs the manifest to the controller and prints an operator hint.  No\n   workspace file is written.\n2. The operator reviews via `gbx stack diff [-p \u003cid\u003e]`, then\n   `gbx stack apply [-p \u003cid\u003e] -y`.  The host CLI applies the\n   controller's stored proposal.\n3. The controller validates the manifest (registry allowlist, safe-cap\n   allowlist for `cap_add`, no host-bind volumes, resource caps), pulls\n   images, creates `glovebox-stack-\u003cproject\u003e` (internal), starts services,\n   and waits for healthchecks. Any failure rolls back fully.\n4. The agent runs `gbx-stack wait` and then talks to services by name\n   (`redis:6379`, `neo4j:7687`, …).\n\nThe agent learns this workflow from two complementary files in\n`defaults/`:\n\n- `docker-sandbox.md` - the long-form operating manual (this whole\n  `gbx-stack` flow, manifest constraints, `cap_add` allowlist, etc.). It\n  is bind-mounted read-only into the container at\n  `/etc/glovebox/docker-sandbox.md`, so any host-side edit is visible\n  to the agent immediately, no restart required.\n- `proxy-sandbox.md` - the long-form operating manual for handling the\n  egress 451 error. Also bind-mounted read-only into the container at\n  `/etc/glovebox/proxy-sandbox.md`.\n- `agent-instructions.md` - a short, ~25-line summary that names\n  the Docker sandbox and teaches the agent the egress 451. It's injected\n  into each agent's conventional instruction file on every `agent.Ensure`\n  pass - i.e. on `gbx new`, `gbx start`, `gbx mount apply`, `gbx rebuild`,\n  … - specifically into `state/\u003cpid\u003e/claude/CLAUDE.md`,\n  `state/\u003cpid\u003e/codex/AGENTS.md`, and `state/\u003cpid\u003e/gemini/GEMINI.md`.\n  It references `docker-sandbox.md` and `proxy-sandbox.md` files containing\n  more comprehansive explanations - all of which are lazily included into\n  the context.\n\nThe injection is wrapped in HTML-comment markers\n(`\u003c!-- glovebox-instructions-begin --\u003e` … `\u003c!-- glovebox-instructions-end --\u003e`):\nthe content *between* markers is refreshed each pass (so changes to the\nsummary land on next ensure), the content *outside* the markers (user\nnotes, project rules) is preserved verbatim. If the target file already\ncontains the markers, the block is replaced in place; if it lacks them,\nthe block is appended; if the file doesn't exist, it's created. Writes\nare atomic and skipped entirely when the resulting bytes match what's\nalready on disk.\n\n#### Agent CLI (`gbx-stack`, in-container)\n\n| Command | Purpose |\n|---|---|\n| `gbx-stack status` | Print health summary. |\n| `gbx-stack info` | JSON service map. |\n| `gbx-stack wait` | Block until services are healthy. |\n| `gbx-stack start \u003csvc\u003e` | Start a service from the live manifest. |\n| `gbx-stack stop \u003csvc\u003e` | Stop a service. |\n| `gbx-stack reset \u003csvc\u003e` | Wipe a service's volumes and restart. |\n| `gbx-stack logs \u003csvc\u003e [--follow]` | Stream a service's logs. |\n| `gbx-stack propose \u003cfile\u003e` | Submit `\u003cfile\u003e` as the proposed manifest (POST to controller). |\n| `gbx-stack diff` | Show live vs proposed. |\n\nThe agent's power stops at the live manifest: it can start, stop, and\nreset services that were already approved, but it cannot add new ones\nwithout the operator running `gbx stack apply` again.\n\n#### Manifest fields\n\nA service entry may set:\n\n| Field | Purpose |\n|---|---|\n| `image` | Fully tagged image (no `:latest`); registry must be on the allowlist. |\n| `env` | Map of env vars; `${FOO}` references must be on the env allowlist. |\n| `volumes` | Named volumes only (`\u003cname\u003e: \u003ccontainer-path\u003e`); host binds rejected. |\n| `healthcheck` | Compose-style `test` / `interval` / `retries` / `timeout`. |\n| `resources` | `cpus` and `memory` caps, bounded by controller limits. |\n| `cap_add` | Linux caps to grant on top of the minimum set; safe allowlist only. |\n\nThe safe-cap allowlist for `cap_add` is `IPC_LOCK`, `SYS_NICE`,\n`SYS_RESOURCE`, `DAC_READ_SEARCH`. Anything else (e.g. `NET_ADMIN`,\n`SYS_ADMIN`, `SYS_PTRACE`) is rejected at apply time.\n\n#### Example\n\nExample manifest draft (e.g. `/tmp/stack.yml`):\n\n```yaml\nversion: 1\nservices:\n  redis:\n    image: redis:7-alpine\n    volumes:\n      data: /data\n  neo4j:\n    image: neo4j:5\n    cap_add: [IPC_LOCK]\n    volumes:\n      data: /data\n```\n\nFrom inside the agent, after the operator has applied it:\n\n```bash\ngbx-stack wait\npython -c \"\nimport redis\nr = redis.Redis(host='redis', port=6379)\nr.set('hello', 'world')\nprint(r.get('hello'))\n\"\ngbx-stack reset redis   # clear data between test runs\n```\n\n#### How `gbx stack apply` works under the hood\n\n`gbx stack apply` is a thin host-side wrapper. The interesting work\nhappens in the `stack-controller` (Go, `docker/stack-controller/`). On\n`POST /projects/\u003cpid\u003e/apply` it does this, in order:\n\n1. **Validate the manifest** against the schema and policy rules\n   (`internal/manifest/`): strict YAML decoding rejects unknown fields,\n   image registries must be on the allowlist, image tags must be present\n   and not `:latest`, volumes must be named (no host binds), `env`\n   references must match the env-var allowlist, `cap_add` entries must\n   be in the safe-cap allowlist, and `resources.cpus` / `resources.memory`\n   must fit the controller's caps (4 CPUs, 8 GiB by default). A\n   validation failure returns a structured error with a `HintForAgent`\n   field surfaced via `gbx-stack wait`.\n\n2. **Plan Docker resources** (`internal/dockerx/compose.go`): the\n   manifest is translated into concrete specs:\n\n   | Resource  | Naming convention                                   |\n   |-----------|-----------------------------------------------------|\n   | Network   | `glovebox-stack-\u003cpid\u003e` (`internal: true`)           |\n   | Container | `glovebox-stack-\u003cpid\u003e-\u003csvc\u003e` with DNS alias `\u003csvc\u003e` |\n   | Volume    | `glovebox-stack-\u003cpid\u003e-\u003csvc\u003e-\u003cname\u003e`                 |\n\n   Healthchecks come from the manifest if declared; otherwise the\n   controller injects a per-image default (`redis-cli ping`,\n   `pg_isready`, `mysqladmin ping`, `rabbitmq-diagnostics ping`,\n   `wget http://localhost:7474/` for neo4j). Anything not on that list\n   gets no healthcheck and is treated as ready on `running`.\n\n3. **Acquire a per-project mutex** so two concurrent applies for the\n   same `pid` serialize cleanly. Different projects don't block each\n   other.\n\n4. **Walk the Docker API** through `socket-proxy`\n   (`tcp://socket-proxy:2375`, on `glovebox-control` only - the agent\n   has no path there):\n\n   1. `EnsureNetwork(glovebox-stack-\u003cpid\u003e, internal=true)`.\n   2. `EnsureVolume(...)` for each named volume.\n   3. `PullImage(...)` for each new container - pulls go out via\n      `glovebox-egress` since the controller is on it.\n   4. `CreateContainer(...)` with the planned name, DNS alias, env,\n      named-volume mounts, healthcheck, resource caps, and minimum\n      capability set plus any allowed `cap_add`.\n   5. `StartContainer(...)`.\n\n5. **Block on healthchecks** by polling each container's\n   `Health.Status` until it's `healthy` (or empty, meaning no\n   healthcheck → treated as ready). Default budget is 60 s; failure\n   triggers rollback.\n\n6. **Attach the agent** to the project network with\n   `ConnectNetwork(glovebox-agent, glovebox-stack-\u003cpid\u003e)` so service\n   DNS names resolve from inside the agent. Done after healthchecks so\n   a failed apply doesn't leave the agent's endpoint blocking the\n   rollback's `RemoveNetwork`.\n\n7. **Persist the live manifest** plus a `last_apply: {status, reason,\n   time}` record to `state/controller/projects.json` (atomic tempfile +\n   rename, mutex-guarded).\n\n8. **Return 200** with `{status: \"applied\", project_id, network,\n   services}`.\n\n**Rollback** is transactional. Each successful create is recorded in an\n`undo` struct; any later failure unwinds in reverse order (containers →\nvolumes → network). The persisted `last_apply` records the rollback\nreason so `gbx-stack wait` can surface \"rejected\" or \"rolled_back\"\ninstead of timing out blindly.\n\n**Reconcile on startup** (`internal/state/reconcile.go`): when the\ncontroller boots, it walks every record in `projects.json` and re-creates\nanything missing - network, volumes, and stopped/deleted containers -\nthen re-attaches `glovebox-agent` to each project's network. So a\n`docker rm`'d service or a host reboot doesn't lose state; running `gbx\nproject start` or other per-project commands will bring everything back\nto the intended shape.\n\n### Environment variables\n\nMost of these are resolved automatically - by the `gbx` binary, by the\nHomebrew wrapper, or by code that runs inside the agent container. The\nones worth knowing about as a user are at the top; the rest are\ndocumented for the rare time you need to override them.\n\n#### User-facing\n\n| Variable | Purpose | Default |\n|---|---|---|\n| `GBX_CONFIG_DIR` | Where `.env`, `allowlist.txt`, the `active-project` pointer, and `state/` live. Override to keep multiple unrelated glovebox setups on one host. | `~/.config/glovebox` |\n| `GBX_CONTROLLER_HOST_PORT` | Host-side port forwarded to the stack-controller's `:7001` listener. Change if `17001` collides with something. | `17001` |\n| `GBX_WAIT_TIMEOUT_S` | Seconds `gbx-stack wait` will poll before giving up. | 1800 |\n| Provider keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, …) | Forwarded into the agent container at create time so the agent CLIs can authenticate. Edit `${GBX_CONFIG_DIR}/.env`. | - |\n\n#### Set inside the agent container\n\n`gbx` injects these so in-container tools (`gbxa`, `gbx-stack`, agent\ncode) know which project they belong to. You usually only see them when\ndebugging from a shell inside the container.\n\n| Variable | Purpose |\n|---|---|\n| `GBX_PROJECT_ID` | The 12-char pid of the project this container serves. |\n| `GBX_PROJECT_DIR` | The workspace path mounted at `/workspace`. |\n| `GBX_CONTROLLER_URL` | Stack-controller URL (`http://stack-controller:7000` over the internal network). |\n\n#### Internal / advanced\n\nYou only need to override these if you're hacking on glovebox itself.\n\n| Variable | Purpose |\n|---|---|\n| `GBX_LIBEXEC` | Path to the package files (`docker/`, `defaults/`, `.env.example`). Auto-resolved from the binary's directory; Homebrew sets it explicitly. |\n| `GBX_STATE_DIR` | Override the state subdir. Default is `${GBX_CONFIG_DIR}/state`. |\n| `GBX_AGENT_IMAGE` | Image tag every project's agent container is built from and run against. Default `glovebox-agent:local`. The rebuild test points this at a throwaway tag (`glovebox-agent-test-aad-$$:local`) so it can't untag the operator's real image. |\n| `GBX_TEST_MODE` | When `1`, every agent container `gbx` creates carries the `io.glovebox.test=1` label. `make clean-tests` (and the bash suite's pre-flight) wipe by that label, no matter what happened to the state dir or workspace dir. Exported automatically by `tests/test_helper.sh`. |\n| `GBX_OVERRIDE_PID` | Set transparently by `gbx -p \u003cpid\u003e \u003ccmd\u003e`; downstream code routes to that project for the one invocation. |\n| `GBX_SKIP_STACK_UP` | When `1`, `gbx new` skips bringing the singleton stack up. Used by tests that inject a fake EnsureAgent. |\n| `GBX_TESTS_STACK_ALREADY_UP` | When `1`, each test file's `stack_up` helper short-circuits because the outer parallel runner already brought the stack up. |\n\n---\n\n## Development\n\n### Make targets\n\n`make help` lists everything; the table below summarises the most-used\ntargets. Many also accept env-var overrides (`FILE=`, `WORKERS=`, `BUMP=`,\n`FORCE=`).\n\n| Target                                          | Purpose                                                                  |\n|-------------------------------------------------|--------------------------------------------------------------------------|\n| `make build`                                    | Compile `bin/gbx` from `cmd/gbx`.                                        |\n| `make man`                                      | Regenerate `share/man/man1/gbx.1` from `docs/gbx.1.md` (needs `go-md2man`). |\n| `make test`                                     | Run the bash integration suite. `FILE=` filters to one or more test files. |\n| `make test-go`                                  | Run the Go unit-test suite (`go test ./...`).                            |\n| `make test-all`                                 | `test-go` + `test` - the pre-push gate.                                  |\n| `make test-parallel`                            | Shard the bash suite across `WORKERS=2` subprocesses (≈3× faster).       |\n| `make lint`                                     | gofmt check, `go vet`, and golangci-lint.                                |\n| `make clean`                                    | Remove `bin/gbx`, the generated man page, and stale `.test-config*` dirs. |\n| `make clean-tests`                              | Wipe test Docker residue (labeled containers, TMPDIR agents, dangling stack nets / images / volumes). Never touches real projects. |\n| `make release`                                  | Bump `version.txt`, commit, tag locally. `BUMP=patch\\|minor\\|major` (default: re-tag current). |\n| `make uninstall`                                | Hard reset: every glovebox container, image, and `${GBX_CONFIG_DIR:-~/.config/glovebox}`. Prompts unless `FORCE=1`. |\n\n### Running tests\n\nThe bash integration suite is implemented as a pure Bash runner (no Bats\ndependency). It exercises the host CLI end-to-end against real Docker\ncontainers, so it expects OrbStack to be running.\n\n#### Single file\n\n```bash\n./scripts/run-tests.sh tests/41-wrapper-cd.sh\nmake test FILE=tests/41-wrapper-cd.sh\n```\n\nYou can also pass multiple files:\n\n```bash\n./scripts/run-tests.sh tests/41-wrapper-cd.sh tests/43-wrapper-run.sh\nmake test FILE=\"tests/41-wrapper-cd.sh tests/43-wrapper-run.sh\"\n```\n\n#### Parallel suite\n\n`make test-parallel` runs the parallel-safe test files across `WORKERS=2`\nsubprocesses (each with its own `GBX_CONFIG_DIR=.test-config.wN`), then\nruns the must-serial bucket - tests that mutate singleton stack state or\ntalk to the controller - sequentially against the shared config dir. The\nsingleton stack is brought up once at the suite boundary instead of\nper-file. Net runtime is ~3× faster than `make test` (≈7 min vs ≈20 min\non a clean M-series Mac).\n\n```bash\nmake test-parallel              # WORKERS=2 (the empirical sweet spot)\nWORKERS=4 make test-parallel    # faster but hits OrbStack daemon contention; expect failures\n```\n\n---\n\n## Maintainers\n\n### Publishing a Homebrew release\n\nThe canonical formula lives in the dedicated tap repo,\n[`okulik/homebrew-glovebox`](https://github.com/okulik/homebrew-glovebox),\nas `Formula/glovebox.rb` (the copy in this repo is a non-authoritative\nreference). It installs from the tagged release tarball pinned by its\n`url`, `sha256`, and `version` lines. To cut a new version:\n\n1. In this repo, tag and push:\n   ```bash\n   git tag v0.1.0\n   git push origin v0.1.0\n   ```\n2. Compute the sha256 of the release tarball GitHub creates automatically:\n   ```bash\n   curl -sL https://github.com/okulik/glovebox/archive/refs/tags/v0.1.0.tar.gz \\\n     | shasum -a 256\n   ```\n3. In the tap repo's `Formula/glovebox.rb`, update the `url`, `sha256`, and\n   `version` lines to match the new tag, then commit and push.\n\nThe formula also carries a `head` spec (latest `main`) for anyone who wants\nto track development with `brew install --HEAD glovebox`.\n\n### Refreshing an install after pushing a new release\n\nAfter bumping the formula, users pick up the new version with:\n\n```bash\nbrew update\nbrew upgrade glovebox\n```\n\nA HEAD install is the exception - the version string doesn't change, so\n`brew upgrade` reports \"already installed\" and skips the rebuild. Force it\nto fetch the new commit:\n\n```bash\nbrew upgrade --fetch-HEAD glovebox\n# or, equivalently:\nbrew reinstall --HEAD glovebox\n```\n\nConfirm the formula commit brew will pull matches your local push with\n`git -C \u003crepo\u003e log -1 --oneline Formula/glovebox.rb` before reinstalling.\n\n---\n\n## Licence\n\nLicensed under [AGPL-3.0-or-later](LICENSE). © 2026 Orest Kulik.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fokulik%2Fglovebox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fokulik%2Fglovebox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fokulik%2Fglovebox/lists"}