Containerized CLI workspace for AI coding agents (Claude Code, Codex CLI) with common developer tools.
- Prerequisites
- Initial setup
- Usage
- Capsule command examples
- Additional features
- Configuration reference
- Run checks and tests
- Included agent tooling
- Security Note
- License
- Docker Engine 24+ and Docker Compose v2
- Access to Claude or Codex.
There is no true quick start for the first run. Capsule persists GitHub auth
state in the home volume, so it is worth doing setup in this order: prepare the
token first, then start Capsule, then verify the workspace and gh auth before
opening Claude or Codex. These checkpoints make later troubleshooting much
easier.
| Phase | What it proves |
|---|---|
| Prepare credentials | The first Capsule run can persist working GitHub auth. |
| Start Capsule | The image builds and the container starts successfully. |
| Verify the container | The workspace mount and persistent home volume work. |
| Verify GitHub auth | gh is already logged in before agent startup. |
| Verify your agent | Claude or Codex can read the workspace. |
-
Decide if you want to use Claude, Codex, or both.
-
Generate a GitHub access token.
-
Make sure that you are logged in.
-
Click on the "Generate new token" button.
-
Confirm access if the UI asks you to do so.
-
Fill in the "New fine-grained personal access token" form:
-
Token name: Choose any name. For example, "Capsule".
-
Fill in the other fields as you see fit. It's ok to leave them on the default values.
-
-
Click on the "Generate token" button below the form.
-
Click on the "Generate token" button in the popup window.
-
Copy the token and save it somewhere safe.
-
You may close the GitHub website.
The token only needs to cover
ghand repository access. Capsule uses it forghauth and as a build secret whenmisedownloads tools from GitHub. Claude and Codex authenticate separately, in Phases 5 and 6. -
Create a project directory that we can use as a test.
$ mkdir /home/myuser/myproject $ cd /home/myuser/myproject $ echo "My favorite color is purple." > AGENTS.md $ echo "My favorite color is purple." > CLAUDE.md -
Create an alias:
alias capsule="/absolute/path/to/casual-capsule/capsule.sh"You might want to add this to your init script (such as
~/.bashrcor~/.zshrc). -
Set
GITHUB_API_TOKENto the value you received from GitHub (replace[GITHUB_API_TOKEN]).Set this before the first build/run so the persistent home volume starts with working GitHub auth.
$ export GITHUB_API_TOKEN=[GITHUB_API_TOKEN]
-
Build the Capsule Docker image and start it in the current directory.
$ capsule --build -
When Capsule asks the following, type "y".
Allow capsule to run in /home/myuser/myproject (y/N)? -
You should see that Docker builds the Capsule image, creates a container and starts it:
$ capsule --build Allow capsule to run in /home/myuser/myproject (y/N)? y [...] [+] build 1/1 ✔ Image hcs-capsule:local Built ✔ Volume casual-capsule_home Created Container casual-capsule-cli-run-4d7e2776d2fd Creating Container casual-capsule-cli-run-4d7e2776d2fd Created user@capsule:/home/workspace$Checkpoint: the base image built and Capsule started successfully.
-
Check your workspace:
user@capsule:/home/workspace$ cat AGENTS.md My favorite color is purple. user@capsule:/home/workspace$ cat CLAUDE.md My favorite color is purple.Your
/home/myuser/myprojectdirectory is mounted to/home/workspaceinside the container.Checkpoint: the workspace bind mount is correct before you start an agent.
-
Check the user home directory:
user@capsule:/home/workspace$ cat /home/user/.config/gh/hosts.yml github.com: users: myuser: oauth_token: [GITHUB_API_TOKEN] oauth_token: [GITHUB_API_TOKEN] user: myuserThe Docker daemon created a
casual-capsule_homeDocker volume when it started the container. This volume is mounted to/home/user. This volume is persistent and shared between Capsule instances. Use-por--private-hometo bind-mount~/.capsule-homeinstead.The
/home/user/.config/gh/hosts.ymlfile should contain your GitHub API token.Checkpoint: the persistent home volume contains the expected GitHub auth configuration.
-
Check that you are logged in to GitHub.
user@capsule:/home/workspace$ gh auth status github.com ✓ Logged in to github.com account myuser (/home/user/.config/gh/hosts.yml) - Active account: true - Git operations protocol: https - Token: [GITHUB_API_TOKEN]Checkpoint:
ghis ready before you open Claude or Codex.If
gh auth statussays that you are not logged in, add your GitHub token togithub_api_token.txt(inside the container) and log in manually:$ gh auth login --with-token < github_api_token.txtYou can do the same when your token expires in the future.
-
Start Claude Code:
$ claude -
Claude asks if you trust the files in
/home/workspace.Choose the response that proceeds in this folder.
-
Log in when Claude prompts you to.
Claude prints a URL to open in your browser. Open it on the host, complete the login, and paste the resulting code back into the container.
Credentials are written to
/home/user, so the persistent home volume keeps you logged in across Capsule sessions. -
Test the connection and that Claude can read
CLAUDE.md:> What is my favorite color? ● Your favorite color is purple.
-
Start Codex:
$ codex -
Select "Sign in with Device Code."
-
Follow the instructions to log in.
-
Codex asks if you trust
/home/workspace.Choose the following response: "Yes, continue".
-
Codex probably prints the following warning:
Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the vendored bubblewrap in the meantime.You can continue this setup, but later you might want to fix this warning. There are at least two ways:
-
One way to eliminate this warning is to run
codexwith--dangerously-bypass-approvals-and-sandbox. This disables the sandbox which would usebubblewrap. -
Another way to eliminate the warning is to use a custom
compose.ymlfile that addsprivileged: Trueto thecliservice, and use a customDockerfilethat installs thebubblewrappackage withapt. See more information about this kind of customization in the Custom Capsule images section.
-
-
Test the connection and that Codex can read
AGENTS.md:› What is my favorite color? • Your favorite color is purple.
Once you set up Capsule, you can start it in any project directory. You can even start Claude or Codex directly:
$ cd /home/myuser/myproject
$ capsule claude
$ capsule codex
Pass a command instead of the default shell:
capsule claude
capsule bash -lc "node -v && python --version"
capsule docker psBuild the image before starting:
capsule --build
capsule -b claudeBuild only the custom image before starting:
CAPSULE_CUSTOM_COMPOSE=/home/myuser/python-capsule/compose.yml \
capsule --build-customUse -- when arguments overlap launcher flags:
capsule -- --build trueCapsule auto-detects the host user's UID/GID via id -u/id -g and
DOCKER_GID from the active Docker socket (falling back to 991 on macOS,
999 on Linux). If UID/GID detection fails (e.g. id is unavailable), it falls
back to 1000:100 and prints a warning. The entrypoint handles UID/GID
adjustment and Docker socket group membership at startup.
This mechanism ensures that the user inside the container can access the
/home/workspace directory and the host's Docker daemon.
You can override UID/GID or DOCKER_GID by using environment variables:
CAPSULE_UID=2000 CAPSULE_GID=2000 capsuleBake a custom UID/GID into the image (avoids runtime chown):
CAPSULE_UID=2000 CAPSULE_GID=2000 capsule --buildOn the first run in a new directory, capsule.sh prompts for explicit approval
and records the approved path in ~/.config/capsule (overridable via
CAPSULE_CONFIG).
When --remote is active, Capsule checks only the remote target in that same
allowlist. Remote targets approved via --remote are stored as
ssh://HOST[:PORT]/path.
Use -p or --private-home to replace the shared Docker volume for
/home/user with a bind mount from ~/.capsule-home on the Docker daemon
host.
capsule --private-home
capsule -p --build
capsule -p -r buildbox:/srv/casual-capsuleThis keeps Capsule state per user instead of per Docker Compose project.
When --remote is active, Capsule resolves ~/.capsule-home on the remote
host. When CAPSULE_HOST_PATH_MAP is active inside a non-Capsule container,
Capsule resolves ~/.capsule-home through that map; if the map does not cover
$HOME, set CAPSULE_HOME_HOST_DIR explicitly.
If you want to extend the Docker image or Compose configuration provided by
Capsule, you can do that by creating a custom compose.yml file and setting its
path in CAPSULE_CUSTOM_COMPOSE.
The custom compose.yml file must override the cli section.
Example layout:
/home/myuser/python-capsule/
|- Dockerfile
`- compose.yml
Example Dockerfile:
FROM casual-capsule-cli:latest
RUN uv tool install blackExample compose.yml:
services:
cli:
image: python-capsule:local
build:
context: ${CAPSULE_CUSTOM_DIR}
dockerfile: ${CAPSULE_CUSTOM_DIR}/Dockerfile
environment:
PYTHON_CAPSULE: "1"Use it like this:
export CAPSULE_CUSTOM_COMPOSE=/home/myuser/python-capsule/compose.yml
./capsule --buildWith a custom compose file, capsule.sh --build first rebuilds the base image
casual-capsule-cli:latest, then builds the merged custom cli image, and
finally starts the container from that merged configuration.
If you only want to rebuild the merged custom cli image, use
capsule.sh --build-custom instead. This flag requires
CAPSULE_CUSTOM_COMPOSE.
The entrypoint reads the GITHUB_API_TOKEN secret on every container
start and calls gh auth login --with-token automatically. To rotate
your token:
-
Export the new token in your shell:
export GITHUB_API_TOKEN=ghp_... -
Start a new Capsule session — the entrypoint handles the rest:
capsule
The updated credentials are written to the persistent home volume and survive subsequent restarts. No rebuild is required.
Use --publish to expose a port from the Capsule container on the Docker
daemon host.
capsule --publish 8080:8080
capsule --publish 8080:8080 --publish 127.0.0.1:9229:9229Use CAPSULE_PUBLISH for repeatable port mappings in environment-based
launchers. Separate mappings with semicolons.
CAPSULE_PUBLISH="8080:8080;127.0.0.1:9229:9229" capsuleUse --volume or -v to add extra bind mounts to the Capsule container. The
mount specification follows Docker's HOST:CONTAINER[:OPTIONS] syntax.
capsule --volume /host/cache:/cache
capsule --volume /host/config:/etc/config:ro
capsule -v /host/data:/data -v /host/cache:/cacheUse CAPSULE_VOLUME for repeatable mounts in environment-based launchers.
Separate mount specs with semicolons.
CAPSULE_VOLUME="/host/data:/data;/host/config:/etc/config:ro" capsuleWhen capsule.sh runs inside an existing container, the path it sees may not
be a path the Docker daemon can mount. Capsule translates the current container
path back to the daemon-host path before asking Docker to create the
/home/workspace bind mount.
Inside a Capsule, do not reset CAPSULE_HOST_WORKDIR. The outer Capsule sets
it to the daemon-host workspace root, and nested launches reuse it
automatically. Use CAPSULE_HOST_PATH_MAP only when the current non-Capsule
container sees the same files under a different absolute path.
CAPSULE_HOST_PATH_MAP=/workspace=/home/myuser/myproject capsule
CAPSULE_HOST_PATH_MAP=/workspace=/src/project:/tmp/cache=/var/cache capsuleCAPSULE_HOST_PATH_MAP is a colon-separated list of
container_prefix=host_prefix pairs. The first matching prefix wins.
Use --remote HOST[:PORT]:/abs/path to build and run on a remote Docker daemon
over SSH. Capsule sets DOCKER_HOST=ssh://HOST[:PORT] for Compose and mounts
/abs/path as /home/workspace on the remote daemon host.
capsule --remote buildbox:/srv/casual-capsule
capsule --remote buildbox:2222:/srv/casual-capsule
capsule --remote buildbox:/srv/casual-capsule --buildThe remote target must be approved first. Use an SSH config host alias when you
need extra SSH options beyond the optional port in HOST[:PORT].
Usage:
capsule.sh [OPTIONS]
capsule.sh [OPTIONS] -- [ARGS]
Options:
-
-b,--build: Rundocker compose build clibeforerun. -
-p,--private-home: Bind-mount~/.capsule-homefrom the Docker daemon host to/home/userin the container. -
--build-custom: Rundocker compose build clionly for the merged custom compose configuration beforerun. RequiresCAPSULE_CUSTOM_COMPOSE. -
-r HOST[:PORT]:/abs/path,--remote HOST[:PORT]:/abs/path: Rundocker composeagainstssh://HOST[:PORT]and mount/abs/pathas/home/workspaceon that remote host. -
--publish HOST[:CONTAINER]: Publish a container port on the host when running the container. May be passed multiple times. -
-v HOST:CONTAINER[:OPTIONS],--volume HOST:CONTAINER[:OPTIONS]: Bind-mount a host path into the runtime container. May be passed multiple times. For example, use:rofor a read-only mount. -
--no-cache: Pass--no-cacheto the build commands triggered by--buildor--build-custom. -
-h,--help: Show usage message. -
--: Stop launcher option parsing; pass remaining arguments todocker compose run cli.
-
CAPSULE_DEBUG: Enable shell xtrace forcapsule.sh.Default: empty. When set to
1,capsule.shruns withset -x. -
CAPSULE_UID: Container user UID (user ID).Default: The output of
id -uon the host. If that doesn't work, then 1000. -
CAPSULE_GID: Container user GID (group ID).Default: The output of
id -gon the host. If that doesn't work, then 100. -
DOCKER_GID: Docker socket GID.Default: Auto-detected from the host.
-
DOCKER_HOST: Docker daemon endpoint fordocker compose.Default: Docker CLI default.
--remotesetsssh://HOST[:PORT]. -
CAPSULE_HOME_HOST_DIR: Host path bound to/home/userwhen--private-homeis active.Default: empty.
--private-homeresolves it to~/.capsule-homeon the Docker daemon host, usingCAPSULE_HOST_PATH_MAPwhen needed. -
CAPSULE_HOST_PATH_MAP: Colon-separatedcontainer_prefix=host_prefixmappings for resolving daemon-host paths from inside non-Capsule containers.Default: empty. The first matching prefix wins, and the mapped host path is used for allowlist checks.
-
CAPSULE_PUBLISH: Semicolon-separated list of--publishspecs.Default: empty. Each non-empty entry is passed before command-line
--publishoptions. -
CAPSULE_VOLUME: Semicolon-separated list ofHOST:CONTAINER[:OPTIONS]--volumespecs.Default: empty. Each non-empty entry is passed before command-line
--volumeoptions. -
CAPSULE_WORKDIR: Workspace directory.Default: current working directory.
-
CAPSULE_CUSTOM_COMPOSE: Optional custom compose override file.Default: empty.
-
CAPSULE_CONFIG: Path to the file that contains the approved directories.Default:
~/.config/capsule. -
CAPSULE_HOST_WORKDIR: daemon-host-visible path for/home/workspace.Default: derived from
CAPSULE_WORKDIR.--remoteoverrides it with the remote workspace path. -
GITHUB_API_TOKEN: Passed as a build secret forghauth and formisetool downloads from GitHub.
Run lint checks on the host:
$ tests/check_all.shRun the test suites on the host:
$ tests/test_all.shRun checks and tests inside a Capsule:
$ capsule tests/check_all.sh
$ capsule tests/test_all.shcheck_all.sh runs dclint, hadolint, and shellcheck on discovered files.
When one of these tools is missing, it prints a warning and skips that linter.
test_all.sh prints each suite name before running it.
-
The fast suite uses command stubs, so it does not require a running Docker daemon.
-
The end-to-end suite builds and runs the real capsule image when Docker and Compose are available. It skips cleanly when the daemon is unavailable.
The end-to-end suite also prints the path to a per-run logfile under
_build/tests/. The logfile is kept after the run and records suite events and plain Docker/Capsule output with UTC timestamps on every line.
The image includes utilities commonly used by coding agents, installed via
mise (configured by the MISE_SYSTEM_TOOLS Dockerfile ARG). Set
MISE_SYSTEM_TOOLS in the build environment to override the default tool list
when building through Compose:
MISE_SYSTEM_TOOLS="bat fd jq ripgrep uv" docker compose build cliclaude: Claude Code agent CLI.codex: Codex agent CLI.bat: Syntax-highlighted file viewing.eza: Enhanced directory listing.fd: Fast file discovery.gh: GitHub CLI operations.jq: JSON filtering and inspection.rg(ripgrep): Fast content search.uv: Python version, tool, and environment management.
Installed via apt:
shellcheck: Shell script linting.tree: Directory structure visualization.
Python tooling (installed via uv; binaries available on PATH via
~/.local/bin):
python: Python runtime (version set byPYTHON_VERSIONARG, default3.14).ruff: Fast Python linter and formatter.ty: Python type checker.
Verify inside capsule:
capsule bash -lc "rg --version && fd --version && jq --version && \
bat --version && eza --version && shellcheck --version && \
gh --version && tree --version && python --version"This setup mounts /var/run/docker.sock into the container, giving it
host-level Docker access. Do not use with untrusted code or shared hosts.
Copyright 2026 Cursor Insight
Licensed under the Apache License, Version 2.0.