{"id":49675527,"url":"https://github.com/source-crafting/claude-cask","last_synced_at":"2026-05-11T06:01:07.912Z","repository":{"id":356068213,"uuid":"1229671052","full_name":"source-crafting/claude-cask","owner":"source-crafting","description":"Run Claude Code in an ephemeral Docker container scoped to one project — your workspace, host login, and one GPG signing key forwarded; nothing else from your machine.","archived":false,"fork":false,"pushed_at":"2026-05-06T14:12:35.000Z","size":219,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-06T14:33:41.385Z","etag":null,"topics":["agent","anthropic","claude","claude-code","cli","developer-tools","docker","gpg","sandbox"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/source-crafting.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":"SECURITY.md","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-05-05T09:20:24.000Z","updated_at":"2026-05-06T12:54:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/source-crafting/claude-cask","commit_stats":null,"previous_names":["source-crafting/claude-cask"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/source-crafting/claude-cask","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/source-crafting%2Fclaude-cask","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/source-crafting%2Fclaude-cask/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/source-crafting%2Fclaude-cask/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/source-crafting%2Fclaude-cask/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/source-crafting","download_url":"https://codeload.github.com/source-crafting/claude-cask/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/source-crafting%2Fclaude-cask/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32719572,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T00:29:05.620Z","status":"online","status_checked_at":"2026-05-07T02:00:07.170Z","response_time":62,"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":["agent","anthropic","claude","claude-code","cli","developer-tools","docker","gpg","sandbox"],"created_at":"2026-05-07T02:02:25.087Z","updated_at":"2026-05-08T03:01:28.338Z","avatar_url":"https://github.com/source-crafting.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"```\n ██████╗██╗      █████╗ ██╗   ██╗██████╗ ███████╗     ██████╗ █████╗ ███████╗██╗  ██╗\n██╔════╝██║     ██╔══██╗██║   ██║██╔══██╗██╔════╝    ██╔════╝██╔══██╗██╔════╝██║ ██╔╝\n██║     ██║     ███████║██║   ██║██║  ██║█████╗      ██║     ███████║███████╗█████╔╝ \n██║     ██║     ██╔══██║██║   ██║██║  ██║██╔══╝      ██║     ██╔══██║╚════██║██╔═██╗ \n╚██████╗███████╗██║  ██║╚██████╔╝██████╔╝███████╗    ╚██████╗██║  ██║███████║██║  ██╗\n ╚═════╝╚══════╝╚═╝  ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝     ╚═════╝╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝\n                  Claude Code, sealed in an ephemeral cask.\n```\n\n[![CI](https://github.com/source-crafting/claude-cask/actions/workflows/ci.yml/badge.svg)](https://github.com/source-crafting/claude-cask/actions/workflows/ci.yml)\n\n# claude-cask\n\nRun Claude Code inside an ephemeral Docker container with the host working directory mounted, the host `~/.claude` config forwarded, Opus by default, safe-mode permission prompts, and signed commits using exactly one of the host's GPG keys (no private key material exposed to the container).\n\n## Requirements\n\n- macOS (Docker Desktop) or Linux with a working Docker daemon\n- `git` configured with at least `user.name` and `user.email` (see *Git identity precedence* below for how local repo config interacts with global)\n- For signed commits: `user.signingkey` set and `gpg-agent` running. On macOS Docker Desktop, file sharing must include `~/.gnupg`.\n\n## Install\n\nClone the repo to wherever you keep tools, then symlink the launcher onto your `PATH`:\n\n```bash\ngit clone git@github.com:source-crafting/claude-cask.git \u003cinstall-dir\u003e\nln -s \u003cinstall-dir\u003e/claude-cask /usr/local/bin/claude-cask\n```\n\nReplace `\u003cinstall-dir\u003e` with the path you cloned into (e.g. `~/tools/claude-cask`, `/opt/claude-cask`). The launcher resolves its own location via the symlink, so it works from any clone path.\n\n## Building the image\n\nThe image is tagged `claude-cask:latest`. The launcher builds it automatically on first invocation. To build (or rebuild) explicitly:\n\n```bash\n# From the cloned repo:\ndocker build -t claude-cask:latest .\n\n# Or via the launcher:\nclaude-cask --rebuild\n```\n\nThe launcher detects when the image is stale and rebuilds automatically:\n\n- The Dockerfile and `entrypoint.sh` are hashed at every launch and compared to the image's `claude-cask.image-hash` label. If they differ (you edited either file), the launcher rebuilds.\n- The host UID/GID are compared to the image's `claude-cask.uid`/`claude-cask.gid` labels. If they differ (you've moved the checkout to a different machine), the launcher rebuilds.\n- After every successful rebuild, dangling claude-cask images are auto-pruned (label-scoped, so other dangling images on your daemon are left alone).\n\nYou only need `--rebuild` to force a rebuild when nothing has changed (e.g., to refresh `@anthropic-ai/claude-code` from npm).\n\n## Usage\n\n```bash\nclaude-cask                       # Opus + safe mode (per-tool prompts apply)\nclaude-cask --auto                # opt into auto mode (no per-tool prompts)\nclaude-cask --model sonnet        # different model\nclaude-cask --rebuild             # rebuild the image before running\nclaude-cask --keep-container      # don't pass --rm; container survives for post-mortem\nclaude-cask -- --resume my-task   # forward args to claude\n```\n\n**Safe by default.** Without `--auto`, the in-container Claude prompts before each tool call (the standard Claude Code behavior). Pass `--auto` only when you trust the AI to act in this workspace without per-action confirmation. See *Security* below for what changes when you do.\n\n**Pre-flight summary.** Each launch prints a summary to stderr — workspace path, `~/.claude` mount, signing key, network, mode, and (on macOS) FileVault status — and, when stdin is a tty, asks `Continue? [Y/n]`. The point is to catch \"I'm in the wrong directory\" mistakes before the container takes hold of the workspace.\n\n**`--keep-container`.** By default `docker run --rm` is used, so when something goes wrong mid-session the container is gone the moment claude exits and there's nothing to `docker logs`. Pass `--keep-container` to drop `--rm` and capture the container id; the launcher prints `docker logs` / `docker inspect` / `docker rm` cleanup hints at exit. You're responsible for `docker rm` when done debugging.\n\n## Managing user extras\n\nAdd OS or language packages to a per-user image layered on top of the\nbase image. Edits live in `~/.config/claude-cask/{apt,npm,pip,cargo}.list`.\n\n```bash\nclaude-cask install --apt   ripgrep jq httpie\nclaude-cask install --npm   prettier\nclaude-cask install --pip   httpie\nclaude-cask install --cargo fd-find\n\nclaude-cask remove  --apt   jq\n\nclaude-cask list                  # show current manifests\nclaude-cask --bare                # launch without the user image\nclaude-cask --rebuild             # rebuild base + user image (no TUI)\nclaude-cask --update-claude-code  # rebuild with the latest claude-code (no TUI)\n```\n\n`install`/`remove`/`list` and the `--rebuild` / `--update-claude-code`\nflags perform their action and exit — they do not launch the Claude TUI.\nThe `:user` image is created lazily on the first `install` and removed\nautomatically when all manifests are emptied.\n\n## What gets mounted\n\n| Host path                     | Container path                                                                                                | Notes                                                                                                                                                                                                                                                                                                                                         |\n| ----------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `$PWD`                        | same path inside the container (e.g. `~/projects/foo`)                                                 | Your project. Mounted at the same path so Claude Code's per-project session storage keys off the real path and matches what host `claude` would record. Working directory is also set to it.                                                                                                                                                  |\n| `~/.claude`                   | `/home/claude-cask/.claude` (plus a symlink at the host home path — see below)                                | Claude Code config dir: settings, sessions, plugins. Mounted read-write so Claude inside the container can manage its own state — install/uninstall plugins, refresh marketplaces, write `enabledPlugins` to `settings.json`, persist sessions. See *Security* below for what this means about trust. The entrypoint also creates a symlink so the host home path (e.g. `/Users/\u003cyou\u003e`) resolves to `/home/claude-cask` inside the container, which makes the absolute paths recorded in `plugins/installed_plugins.json` and `plugins/known_marketplaces.json` resolve correctly so installed plugins (and their skills) load. |\n| `~/.claude.json` (if present) | `/home/claude-cask/.claude.json`                                                                              | Theme and user-level Claude Code config. Read-write. Mount is skipped silently if the host file is absent.                                                                                                                                                                                                                                    |\n| `gpg-agent` extra socket      | `/run/host-gpg-agent`, symlinked into `~claude-cask/.gnupg/S.gpg-agent` after the entrypoint chowns the bind-mount to claude-cask | Signing happens on host; container has no private key access.                                                                                                                                                                                                                                                                                 |\n| Single armored public key     | `/tmp/signing-key.asc` (read-only)                                                                            | Only the configured signing key.                                                                                                                                                                                                                                                                                                              |\n\n## Login state\n\nThe in-container Claude reads its OAuth token from `~/.claude/.credentials.json`, which lives on the host and is bind-mounted into the container via the `~/.claude` directory mount. Token refreshes inside the container write back to the host file, so they persist across runs.\n\nIf that file doesn't exist yet, run `/login` inside the container on the first launch. The OAuth flow writes the file via the mount, and subsequent runs are logged in. This is the same first-run path on Linux and macOS.\n\nThe credentials file persists on disk indefinitely. On macOS, with FileVault on and an encrypted Time Machine destination, this is comparable in security to a keychain entry; see [SECURITY.md](SECURITY.md) for the threat model.\n\n**On macOS, the in-container session is independent from host `claude`.** Host `claude` on macOS authenticates against the Keychain; the in-container Claude authenticates against `~/.claude/.credentials.json`. They are two separate OAuth sessions on the same Anthropic account, each refreshing its own tokens. Logging out on one side does not affect the other.\n\n## Terminal compatibility\n\nClaude Code adapts its keybindings (notably Shift/Ctrl+Enter for inserting a newline) based on the terminal program running it. claude-cask forwards `TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, and `COLORTERM` from the host into the container so the in-container Claude sees the same terminal as on the host (iTerm, Ghostty, VS Code, etc.) and uses matching key sequences.\n\nIf a variable is unset on the host, it's not forwarded. `TERM` itself is set automatically by `docker run -t`.\n\n## Container user\n\nThe container runs as a non-root user `claude-cask` whose **UID/GID match the host user** running the launcher. At image-build time the launcher passes `--build-arg USER_UID=$(id -u) USER_GID=$(id -g)`, the Dockerfile creates the user accordingly (deleting whichever existing user/group occupies that UID/GID — typically the base image's `node` user at 1000), and stamps the image with `claude-cask.uid` / `claude-cask.gid` labels. On subsequent launches the labels are checked against the host UID/GID and a rebuild is triggered automatically if they diverge (e.g., you've moved the checkout to a different machine).\n\nThe entrypoint runs briefly as root to chown the bind-mounted gpg-agent socket and symlink it into `~claude-cask/.gnupg/` (see *GPG security model* below), then drops privileges to `claude-cask` via `gosu` and re-execs itself. By the time `claude` actually starts, the process is `claude-cask` at the host UID. No long-running root process remains in the container.\n\nThis means bind-mounted files are owned by the same UID inside the container as on the host, both on Docker Desktop / macOS (where virtiofs would translate anyway) and on native Linux (where it's the only thing that makes the bind-mounts writable). The launcher refuses to run as host UID 0 (root).\n\n## Git identity precedence\n\nAll four git config values the launcher reads — `user.name`, `user.email`, `user.signingkey`, `commit.gpgsign` — follow git's normal precedence inside the launched workspace: a value set in the local repo config (e.g. `git config --local user.signingkey ABCD1234`) overrides the global value. This lets each repo have its own identity and signing key without juggling global config.\n\nThe launcher's pre-flight summary prints the resolved signing key before starting the container, so a per-repo override is always auditable: if a repo's `.git/config` selects a key you didn't expect, you'll see it and can abort at the `Continue? [Y/n]` prompt. The host keyring still has to actually contain whichever key the resolution lands on — the launcher only forwards keys you already trust on the host.\n\n## GPG security model\n\nThe container never sees:\n- Private key material\n- Any host pubring data\n- Knowledge of any GPG keys other than the one configured signing key\n\nThe container can sign commits using the host's `gpg-agent` because:\n- Its keyring contains the public key for the one configured signing key\n- The host's `gpg-agent` *extra* socket is bind-mounted at `/run/host-gpg-agent`. Docker Desktop on macOS presents that bind-mount as `root:root` mode 660 inside the container, so the entrypoint (running briefly as root) `chown`s it to `claude-cask` and creates a symlink at `~claude-cask/.gnupg/S.gpg-agent`. The unprivileged `claude-cask` user then connects directly to the host's agent through that path. The chown/symlink only changes the container's view; it doesn't touch host-side ownership. No long-running root process or proxy is involved.\n\nIf `user.signingkey` resolves to nothing (neither the workspace's local repo config nor the global config sets it), no GPG mounts are added and signing simply isn't available inside the container.\n\n## Security\n\nclaude-cask sandboxes Claude Code so it can work on the project in `$PWD` without reaching the rest of your machine. The full threat model — what the container does and doesn't bound, the nuances of auto-mode, and the per-flag mitigations — is in [SECURITY.md](SECURITY.md). Read it before turning on `--auto`.\n\nQuick summary:\n- Default (no flags) is safe mode — Claude prompts for each tool call.\n- `--auto` skips per-tool prompts; the AI runs inside the container's bounds without confirmation.\n- The container has full outbound network access by default, same as native Claude Code. If you need stricter egress, use Docker's own `--network` controls or run on a host-side firewalled network.\n- `~/.claude` is mounted read-write. That means an in-container AI can, in principle, modify shared Claude state — install a plugin, edit `settings.json` (`enabledPlugins`, hooks, permissions), write `plugins/cache/` content — that a subsequent **host** `claude` session would then execute. claude-cask trusts the in-container AI not to do this; the alternative (locking these paths read-only) breaks normal plugin install/refresh inside the container. If you want stricter isolation, layer your own RO bind-mounts on `~/.claude/plugins/cache/` and `~/.claude/settings.json` and accept that plugins won't be manageable from inside the container.\n\n## Tests\n\n```bash\nbats tests/claude-cask.bats           # unit\nbats tests/integration/smoke.bats    # integration (builds image)\nshellcheck claude-cask entrypoint.sh  # lint\n```\n\n---\n\nclaude-cask is an unofficial wrapper. It launches Claude Code inside a container but does not modify it or distribute it. Claude Code itself is a product of Anthropic and your use of it through this tool is subject to Anthropic's [usage policies](https://www.anthropic.com/legal/aup) and the terms applicable to your Claude account. This project is not affiliated with or endorsed by Anthropic.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsource-crafting%2Fclaude-cask","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsource-crafting%2Fclaude-cask","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsource-crafting%2Fclaude-cask/lists"}