LockBox is a canonical egress policy and launcher toolkit for hardened coding-agent
sandboxes. It keeps the firewall, HTTPS allowlist proxy, and shared launcher logic in
one repository, then vendors reviewed copies into every managed .devcontainer.
The repository also contains two ready-to-use sandboxes:
.devcontainer/is the environment for developing LockBox itself.sandbox/.devcontainer/is a general-purpose sandbox that can open another local project.
Both support Claude Code and OpenAI Codex through explicit provider entry points. The selected provider changes the agent process and its private configuration volume, not the network policy, read-only Git mounts, or launch-integrity checks.
Copying security scripts between repositories creates silent drift. A firewall fix can land in one sandbox while older copies remain elsewhere. LockBox replaces those independent copies with a small set of canonical files and a deterministic sync check.
LockBox provides:
- a fail-closed IPv4 and IPv6 egress firewall;
- a CONNECT-only Squid proxy with a hostname allowlist;
- a shared base allowlist with per-sandbox overlays;
- reusable host-side launcher helpers;
- provenance stamps on vendored files;
- pinned and verified agent toolchains for the in-repo images;
- checks for vendoring drift, provider parity, and accidentally staged secrets; and
- an optional smoke test that boots the image and proves the egress lock works.
LockBox applies two independent outbound controls when a container starts:
init-firewall.shsets default-DROP packet rules. Only the dedicated Squid user may create outbound connections. Other processes can reach the proxy over loopback but cannot connect directly to the network. IPv6 is default-deny, and cloud metadata addresses are blocked before the proxy exception.squid.confaccepts HTTPSCONNECTrequests only. It reads the TLS Server Name Indication (SNI), splices allowlisted connections without decrypting them, and terminates everything else. End-to-end TLS remains intact; LockBox does not install a man-in-the-middle certificate authority.
Startup succeeds only after the packet rules are verified and
/run/egress-firewall-ok is written. If Squid stops, the packet layer still prevents
direct egress while the entrypoint supervises the proxy.
Additional controls include:
- a non-root
devuser and nosudo; - all set-user-ID and set-group-ID bits removed from the image;
- all Linux capabilities dropped, with only the small startup set added back;
- read-only overlays for
.devcontainer,.git, and any relocated Git hooks path; - no Git push credential inside an ordinary sandbox;
- private, provider-specific configuration volumes; and
- a launch gate that rejects drift in pinned tools.
Read .devcontainer/README.md for the full threat model and
runtime details.
The host launchers use Apple's container, not
Docker Compose or the Visual Studio Code Dev Containers extension. On macOS, install
the runtime and start its system service:
container system startDocker can run the egress smoke test in continuous integration, but it is not the
primary host runtime. The repository itself is Bash and container infrastructure; it
has no application build step or root package.json.
Enable the tracked pre-commit checks once after cloning:
make setupGit does not preserve core.hooksPath across clones. Without this step, the tracked
secret audit and vendoring drift check do not run before commits.
To work on LockBox inside its own sandbox:
.devcontainer/bin/claude
.devcontainer/bin/codexTo open another project in the general-purpose sandbox, invoke the LockBox launcher from that project's working directory:
cd /path/to/project
/path/to/LockBox/sandbox/.devcontainer/bin/dev-claude
/path/to/LockBox/sandbox/.devcontainer/bin/dev-codexThe provider-neutral entry point is also available when selection must come from the environment:
DEV_SANDBOX_AGENT=codex /path/to/LockBox/sandbox/.devcontainer/bin/devEach provider uses a separate container and native configuration volume. A launcher refuses to reuse a container created for a different provider.
Edit the canonical source in this repository. Do not edit a vendored or generated copy
inside a managed .devcontainer.
| Path | Purpose |
|---|---|
init-firewall.sh |
Canonical packet-level egress lock |
squid.conf |
Canonical CONNECT-only SNI allowlist proxy |
base-allowlist.txt |
Minimum hostname set shared by every managed sandbox |
launcher-common.sh |
Shared host-side launcher, staging, mount, and cleanup helpers |
vendored-files.txt |
Manifest of files copied into each managed sandbox |
paths.sh |
Authoritative list of managed .devcontainer directories |
sync.sh |
Vendors canonical files and generates effective allowlists; --target=/absolute/managed/.devcontainer restricts regeneration to one registered target |
audit.sh |
Scans the Git index for secrets and private keys |
tool-pins.env |
Reviewed versions and hashes for baked tools |
bump-pins.sh |
Reports, validates, and deliberately updates tool pins |
test/egress-smoke.sh |
Boots an image and tests the enforced network policy |
VERSION / CHANGELOG.md |
Release identity and history |
Each vendored file contains the canonical content plus a deterministic stamp with the
LockBox version and source SHA-256. ./sync.sh --check regenerates the expected copy
and compares it byte for byte.
Every effective allowlist is generated from two inputs:
base-allowlist.txt
+ <managed .devcontainer>/allowlist.extra.txt
= <managed .devcontainer>/allowlist.txt
- Put a host in
base-allowlist.txtonly when every managed sandbox needs it. - Put project-specific package registries, APIs, and services in that sandbox's
allowlist.extra.txt. - Add a comment explaining why each new host is required.
- Never hand-edit
allowlist.txt; it is generated.
After changing either input, run ./sync.sh. Most managed images bake the effective
allowlist and must be rebuilt. The general-purpose sandbox bind-mounts its effective
list and reloads Squid when the content changes.
The general-purpose sandbox can also consider a target workspace's
.sandbox-allowlist.txt. Because that file belongs to the project being opened, an
interactive launch asks for confirmation before widening egress. Non-interactive
launches ignore the untrusted overlay and remain fail-closed.
paths.sh is the single source of truth for all .devcontainer directories managed by
sync.sh, audit.sh, and the pin checks. Register a new sandbox there before relying
on LockBox to update it. Missing targets are errors by default because silently
skipping a moved repository would leave stale security controls in place.
The checked-in file contains the local deployment layout. Adapt its target list for your environment, or use its environment overrides where appropriate:
| Variable | Purpose |
|---|---|
CODE_ROOT |
Root directory used by managed target paths |
EGRESS_REPO |
LockBox checkout root; normally detected automatically |
EGRESS_SELF_ONLY=1 |
Limit checks to the in-repo generic sandbox, as used in CI |
./sync.sh --allow-missing can downgrade missing targets to warnings for an explicit,
temporary partial sync. Do not use it as the normal deployment path.
Claude and Codex share the image and security boundary but keep separate runtime credentials.
Claude receives a token at launch and uses a sanitized configuration stage. Raw host
configuration is not mounted into the container. Codex never mounts host ~/.codex;
on its first interactive launch it uses the official device-code flow. An
OPENAI_API_KEY or enterprise CODEX_ACCESS_TOKEN can bootstrap the corresponding
non-browser login instead. The resulting Codex cache remains in its private container
volume.
No provider credential is baked into the image or copied into the workspace. A running agent can access its own credential, so revoke that credential if the sandbox is suspected to be compromised.
Repository guidance is exposed through AGENTS.md for Codex and CLAUDE.md for
Claude.
The in-repo images bake Claude Code, Codex, Node.js, Python, and safe-chain. They do not fetch the latest agent CLI at runtime. Updates are resolved during a reviewed rebuild, after a cooldown period, and the native agent binaries are verified by SHA-256.
Useful targets:
| Command | Purpose |
|---|---|
make pins-report |
Compare pinned, cooldown-eligible, and upstream versions; uses the network |
make pins |
Resolve and write reviewed, cooldown-eligible versions and hashes |
make pins-check |
Verify offline that pins match every managed image |
The default cooldown is seven days. Node.js remains within its pinned major version; crossing a major version is a deliberate platform decision. Some operating-system packages are not version-pinned because mirrors remove old builds, so the launch gate records and verifies the binaries present in the built image instead.
For a canonical firewall, proxy, or launcher change:
# Edit the canonical root-level file, then propagate it.
./sync.sh
# Run the portable checks.
shellcheck *.sh .devcontainer/bin/* sandbox/.devcontainer/bin/*
make check
make auditFor an allowlist change, edit the base or one target's overlay, then run the same sync and checks. Rebuild the affected sandbox and run its doctor command before relying on the new policy.
For changes to firewall rules, proxy semantics, capabilities, mounts, or allowed egress, also run the functional test on a host with a supported container runtime:
make testThe smoke test confirms that the firewall sentinel exists, an off-allowlist host is blocked, an allowlisted host is reachable, and a non-CONNECT cleartext request is refused.
A consuming Dockerfile copies the generated files to stable paths:
COPY init-firewall.sh /usr/local/sbin/egress-firewall
COPY squid.conf /etc/squid/squid.conf
COPY allowlist.txt /etc/squid/allowlist.txtIts root entrypoint must run /usr/local/sbin/egress-firewall, verify
/run/egress-firewall-ok, start Squid, and only then continue the container lifecycle.
If the sandbox exposes a service, bake the allowed TCP ports into
/etc/egress/inbound-ports, one port per line. An absent file means no inbound service
ports are opened.
The host launcher must preserve the read-only .devcontainer, .git, and Git hooks
mounts and use the minimal capability set documented in
.devcontainer/README.md. Treat those details as security
invariants, not optional examples.
For a release, update VERSION and CHANGELOG.md, run ./sync.sh so every provenance
stamp changes, complete the required checks, and create the matching v<VERSION> tag
through the repository's authorized publication workflow.
Review REVIEW.md before proposing or publishing any change. It is the
security checklist for this repository.