Skip to content

Repository files navigation

💊 Casual Capsule

ci License Base image Shell Shellcheck Tooling Tooling

Containerized CLI workspace for AI coding agents (Claude Code, Codex CLI) with common developer tools.

Table of contents

📋 Prerequisites

  • Docker Engine 24+ and Docker Compose v2
  • Access to Claude or Codex.

🚀 Initial setup

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.

Phase 1: Prepare credentials

  1. Decide if you want to use Claude, Codex, or both.

  2. Generate a GitHub access token.

    1. Open https://github.com/settings/personal-access-tokens.

    2. Make sure that you are logged in.

    3. Click on the "Generate new token" button.

    4. Confirm access if the UI asks you to do so.

    5. 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.

    6. Click on the "Generate token" button below the form.

    7. Click on the "Generate token" button in the popup window.

    8. Copy the token and save it somewhere safe.

    9. You may close the GitHub website.

    The token only needs to cover gh and repository access. Capsule uses it for gh auth and as a build secret when mise downloads tools from GitHub. Claude and Codex authenticate separately, in Phases 5 and 6.

  3. 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
    
  4. Create an alias:

    alias capsule="/absolute/path/to/casual-capsule/capsule.sh"
    

    You might want to add this to your init script (such as ~/.bashrc or ~/.zshrc).

  5. Set GITHUB_API_TOKEN to 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]
    

Phase 2: Start Capsule

  1. Build the Capsule Docker image and start it in the current directory.

    $ capsule --build
    
  2. When Capsule asks the following, type "y".

    Allow capsule to run in /home/myuser/myproject (y/N)?
    
  3. 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.

Phase 3: Verify the container

  1. 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/myproject directory is mounted to /home/workspace inside the container.

    Checkpoint: the workspace bind mount is correct before you start an agent.

  2. 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: myuser
    

    The Docker daemon created a casual-capsule_home Docker volume when it started the container. This volume is mounted to /home/user. This volume is persistent and shared between Capsule instances. Use -p or --private-home to bind-mount ~/.capsule-home instead.

    The /home/user/.config/gh/hosts.yml file should contain your GitHub API token.

    Checkpoint: the persistent home volume contains the expected GitHub auth configuration.

Phase 4: Verify GitHub auth

  1. 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: gh is ready before you open Claude or Codex.

    If gh auth status says that you are not logged in, add your GitHub token to github_api_token.txt (inside the container) and log in manually:

    $ gh auth login --with-token < github_api_token.txt
    

    You can do the same when your token expires in the future.

Phase 5: Verify Claude (optional)

  1. Start Claude Code:

    $ claude
    
  2. Claude asks if you trust the files in /home/workspace.

    Choose the response that proceeds in this folder.

  3. 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.

  4. Test the connection and that Claude can read CLAUDE.md:

    > What is my favorite color?
    ● Your favorite color is purple.
    

Phase 6: Verify Codex (optional)

  1. Start Codex:

    $ codex
    
  2. Select "Sign in with Device Code."

  3. Follow the instructions to log in.

  4. Codex asks if you trust /home/workspace.

    Choose the following response: "Yes, continue".

  5. 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 codex with --dangerously-bypass-approvals-and-sandbox. This disables the sandbox which would use bubblewrap.

    • Another way to eliminate the warning is to use a custom compose.yml file that adds privileged: True to the cli service, and use a custom Dockerfile that installs the bubblewrap package with apt. See more information about this kind of customization in the Custom Capsule images section.

  6. Test the connection and that Codex can read AGENTS.md:

    › What is my favorite color?
    • Your favorite color is purple.
    

💡 Usage

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

⌨️ Capsule command examples

Pass a command instead of the default shell:

capsule claude
capsule bash -lc "node -v && python --version"
capsule docker ps

Build the image before starting:

capsule --build
capsule -b claude

Build only the custom image before starting:

CAPSULE_CUSTOM_COMPOSE=/home/myuser/python-capsule/compose.yml \
  capsule --build-custom

Use -- when arguments overlap launcher flags:

capsule -- --build true

🧩 Additional features

UID and GID detection

Capsule 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 capsule

Bake a custom UID/GID into the image (avoids runtime chown):

CAPSULE_UID=2000 CAPSULE_GID=2000 capsule --build

Directory approval list

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

Private home bind mount

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-capsule

This 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.

Custom Capsule images

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 black

Example 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 --build

With 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.

Updating your GitHub token

The entrypoint reads the GITHUB_API_TOKEN secret on every container start and calls gh auth login --with-token automatically. To rotate your token:

  1. Export the new token in your shell:

    export GITHUB_API_TOKEN=ghp_...
  2. 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.

Port publishing

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:9229

Use CAPSULE_PUBLISH for repeatable port mappings in environment-based launchers. Separate mappings with semicolons.

CAPSULE_PUBLISH="8080:8080;127.0.0.1:9229:9229" capsule

Runtime volume mounts

Use --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:/cache

Use CAPSULE_VOLUME for repeatable mounts in environment-based launchers. Separate mount specs with semicolons.

CAPSULE_VOLUME="/host/data:/data;/host/config:/etc/config:ro" capsule

Bind mounts in containers started in a Capsule

When 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 capsule

CAPSULE_HOST_PATH_MAP is a colon-separated list of container_prefix=host_prefix pairs. The first matching prefix wins.

Remote Docker host

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 --build

The 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].

🔧 Configuration reference

Command line options

Usage:

capsule.sh [OPTIONS]
capsule.sh [OPTIONS] -- [ARGS]

Options:

  • -b, --build: Run docker compose build cli before run.

  • -p, --private-home: Bind-mount ~/.capsule-home from the Docker daemon host to /home/user in the container.

  • --build-custom: Run docker compose build cli only for the merged custom compose configuration before run. Requires CAPSULE_CUSTOM_COMPOSE.

  • -r HOST[:PORT]:/abs/path, --remote HOST[:PORT]:/abs/path: Run docker compose against ssh://HOST[:PORT] and mount /abs/path as /home/workspace on 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 :ro for a read-only mount.

  • --no-cache: Pass --no-cache to the build commands triggered by --build or --build-custom.

  • -h, --help: Show usage message.

  • --: Stop launcher option parsing; pass remaining arguments to docker compose run cli.

Environment variables

  • CAPSULE_DEBUG: Enable shell xtrace for capsule.sh.

    Default: empty. When set to 1, capsule.sh runs with set -x.

  • CAPSULE_UID: Container user UID (user ID).

    Default: The output of id -u on the host. If that doesn't work, then 1000.

  • CAPSULE_GID: Container user GID (group ID).

    Default: The output of id -g on 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 for docker compose.

    Default: Docker CLI default. --remote sets ssh://HOST[:PORT].

  • CAPSULE_HOME_HOST_DIR: Host path bound to /home/user when --private-home is active.

    Default: empty. --private-home resolves it to ~/.capsule-home on the Docker daemon host, using CAPSULE_HOST_PATH_MAP when needed.

  • CAPSULE_HOST_PATH_MAP: Colon-separated container_prefix=host_prefix mappings 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 --publish specs.

    Default: empty. Each non-empty entry is passed before command-line --publish options.

  • CAPSULE_VOLUME: Semicolon-separated list of HOST:CONTAINER[:OPTIONS] --volume specs.

    Default: empty. Each non-empty entry is passed before command-line --volume options.

  • 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. --remote overrides it with the remote workspace path.

  • GITHUB_API_TOKEN: Passed as a build secret for gh auth and for mise tool downloads from GitHub.

🧪 Run checks and tests

Run lint checks on the host:

$ tests/check_all.sh

Run the test suites on the host:

$ tests/test_all.sh

Run checks and tests inside a Capsule:

$ capsule tests/check_all.sh
$ capsule tests/test_all.sh

check_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.

🤖 Included agent tooling

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 cli
  • claude: 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 by PYTHON_VERSION ARG, default 3.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"

🔐 Security Note

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.

📄 License

Copyright 2026 Cursor Insight

Licensed under the Apache License, Version 2.0.

About

Containerized CLI workspace for AI coding agents, with Node, Go, Docker CLI, and agent utilities.

Resources

Stars

1 star

Watchers

1 watching

Forks

Used by

Contributors

Languages