- Shell 90.2%
- Dockerfile 9.8%
| .forgejo/workflows | ||
| images | ||
| installer | ||
| lib | ||
| tests | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| build.sh | ||
| bump-deps.sh | ||
| install.sh | ||
| README.md | ||
| scc.sh | ||
| scc.sh.version | ||
scc
⚠️ Not affiliated with or endorsed by Anthropic. Claude and Claude Code are trademarks of Anthropic, PBC. This is an unofficial third-party wrapper.
Sandboxed Docker environment for running Claude Code against a workspace
without giving it access to the host. Installed CLI command: scc.
Supply chain: published images are built without provenance or SBOM attestations (
--provenance=false --sbom=false) — there is no signed proof of where or how they were built. If that matters to you, build locally from this repo (./build.sh) instead of pulling the published tags. Will be implemented soon.
Quick Start
Install (macOS or Linux):
curl -fsSL https://forge.stacktop.network/openstacktop/scc/raw/branch/main/installer/i.sh | bash
Then run it against any project:
cd /your/project
scc
See Running for all flags and options.
Migrating from
sclaude? State now lives in~/.scc/. The launcher auto-renames~/.sclaude/on first run if present.
Local Setup Instructions
./install.sh # link scc into ~/bin
./build.sh # build the default flavor (Fedora + Claude + Python/Rust/Go)
scc . # run Claude Code with $PWD mounted at /workspace
Claude state (~/.claude, .claude.json) lives in ~/.scc/ on the host and survives container restarts.
Testing
Tests use bats-core and live under tests/unit/ (fast,
isolated) and tests/feature/ (end-to-end, slower).
Run the whole suite locally:
bats --recursive tests/
Just the unit tests (what pre-commit runs):
bats tests/unit/
Pre-commit hooks
The repo ships a pre-commit config that runs shellcheck + the unit-test
suite on every git commit. Install once per clone:
pip install pre-commit
pre-commit install
pre-commit run --all-files re-runs everything against the whole tree.
CI
Tests run automatically on every push to dev and on every pull request via
.forgejo/workflows/test.yml. The release/build pipeline (build.yml) declares
needs: test, so a tag push won't produce published images unless the test suite is green.
Passing args to claude
Anything you'd normally pass to claude on the host can be forwarded into the container with
--ca (alias --claude-args):
scc --ca "--print 'review this CHANGELOG'" # one-shot non-interactive run
scc --ca "--model sonnet" --ca "--output-format json" # repeat to accumulate args
scc --ca="--debug" . # also accepts --ca=VALUE
scc -c . -- --some-claude-flag # `--` appends literally, no tokenization
Tokenization is shell-style: single/double quotes group, backslashes escape. $VAR and
$(…) are NOT expanded — that's a security property, not an oversight, since project configs
feed the same parser.
Project-pinned defaults live in .scc/config.ini under claude_args = … (see
Project configs below). Project args are prepended before any --ca you
type on the CLI, so claude's own arg parsing makes the CLI override.
Denylist (security gate)
A small set of risky flags are rejected before reaching claude:
| Flag | Reason |
|---|---|
--dangerously-skip-permissions |
bypasses claude's tool-use confirmations |
--mcp-config, --mcp-server |
loads arbitrary code into claude's tool surface |
--add-dir |
expands filesystem access beyond /workspace |
--proxy, --proxy-anyway |
exfiltration channel |
Both bare (--flag) and =value (--flag=foo) forms are caught. There is no y/N prompt —
the denylist is a hard reject. To override, set SCC_ALLOW_DANGEROUS_FLAGS=1 for that one
invocation:
SCC_ALLOW_DANGEROUS_FLAGS=1 scc --ca "--dangerously-skip-permissions"
The override is per-invocation by design — it forces you to type the env var into your shell history, which is harder to do absent-mindedly than clicking through a prompt.
Hard limits
- max 64 total args
- max 8 KB total bytes
Either limit hit → reject with rc=2. Bounds the blast radius of a runaway config or any future argv-parsing bug in claude itself.
Image flavors
Each flavor lives under images/<distro>/<flavor>/ and ships:
Dockerfileconfig.ini— declares this flavor's tag and revision
The default flavor is selected by the images/default/default symlink:
images/
├── default/default → ../fedora/python-pip-rust-golang (symlink)
├── alpine/
│ └── alpine/
│ ├── Dockerfile
│ └── config.ini
└── fedora/
└── python-pip-rust-golang/
├── Dockerfile
└── config.ini
config.ini example:
[image]
tag = python-rust-go-fedora
arches = linux/amd64,linux/arm64
tag is the human-readable flavor token used in image tags. arches is the csv list of
platforms this flavor can be built for; build.sh -a intersects with it and skips flavors
with no overlap. The version part of the image tag is project-wide and resolved by build.sh
(see Versioning below), not per-flavor.
Adding a flavor
cp -r images/fedora/python-pip-rust-golang images/fedora/<new-flavor>
# edit images/fedora/<new-flavor>/Dockerfile
# edit images/fedora/<new-flavor>/config.ini (set a new tag)
ln -sfn ../fedora/<new-flavor> images/default/default # optional: make it the default
Versioning
build.sh owns the project version (independent of the Claude binary's version,
since the binary is sideloaded at runtime). Both image tags and git tags use bare
semver (no v prefix). Resolution priority:
-V X.Y.Zflag (a leadingvis silently stripped)$SCC_VERSIONenv var- The exact-match git tag at HEAD (e.g. when CI runs on a tag push)
- Auto-increment the patch from the latest semver git tag (e.g.
1.0.5→1.0.6) - Default to
1.0.0if no tags exist
So a fresh ./build.sh on main after 1.0.5 produces 1.0.6 automatically;
explicit overrides win when needed.
Tag scheme
Every build produces two tags per flavor (an exact pin and a rolling "latest of this flavor" pointer). Builds of the default flavor produce two extra short aliases on top:
scc:<version>-<flavor> # always — e.g. scc:1.0.6-alpine
scc:<flavor>-latest # always — e.g. scc:alpine-latest
scc:<version> # default only — e.g. scc:1.0.6
scc:latest # default only
Pick by intent:
- Reproducible build inputs → use the exact
scc:<version>-<flavor>pin (e.g.scc:1.0.6-alpine). Pinned to one image digest forever. - Track the latest of one flavor → use
scc:<flavor>-latest(e.g.scc:alpine-latest). New releases overwrite this tag. - Track the default flavor → use
scc:latestorscc:<version>.
Switching the images/default/default symlink changes which flavor owns the
:latest and short tags.
Claude binary handling
The Claude Code binary is not baked into the published image. The container
entrypoint downloads it from claude.ai/install.sh on first run and stores it
in a dedicated Docker named volume (scc-claude, mounted at
/opt/scc-claude). Subsequent scc invocations detect the cached binary
in the volume and skip the install.
Why a named volume instead of a host bind mount: Docker manages it, there's no
host filesystem clutter under ~/.scc/, and the volume layout is independent
of the host OS. Login state (.credentials.json, recent projects, etc.) still
lives on the host bind at ~/.scc/dot-claude/ so it remains visible.
- First
sccafter install: ~236 MB download (3 s on gigabit, 20–30 s on typical home wifi), then runs. - Every subsequent run: zero download, instant start.
- Parallel containers: safe —
flockin the entrypoint serializes the one-time install if two cold-cache containers race. - Force a refresh (e.g. when a new Claude version drops):
SCC_FORCE_REINSTALL=1 scc . - Wipe the cached binary:
docker volume rm scc-claude - Override volume name:
SCC_CLAUDE_VOLUME=my-vol scc . - Fresh state (
scc -c): doesn't mount the volume either, so Claude is re-installed every run.
Daily Claude update check
scc.sh checks once a day whether a newer Claude Code release is published
on npm (@anthropic-ai/claude-code/latest, ~1 KB JSON, 1 s timeout) and
compares it against the version cached in your scc-claude volume. When
the upstream is newer:
⚠ Claude Code update available: 1.0.122 → 1.0.135
refresh on this run with: SCC_FORCE_REINSTALL=1 scc
auto-refresh on daily check: export SCC_AUTO_UPDATE_CLAUDE=1
silence this check: export SCC_NO_CLAUDE_UPDATE_CHECK=1
Reinstall Claude on this run? [y/N]
How it knows the installed version: the entrypoint writes claude --version
to /opt/scc-claude/version and mirrors it to ~/.scc/volume-meta/claude_version
(bind-mounted from the host) on every install. The launcher reads that mirror —
no docker-exec roundtrip on the check.
Controls:
SCC_AUTO_UPDATE_CLAUDE=1— when newer detected, auto-setSCC_FORCE_REINSTALL=1for this run; no prompt.SCC_NO_CLAUDE_UPDATE_CHECK=1— skip the daily probe entirely.- Non-TTY runs (CI, pipes) skip the prompt and continue with the cached binary — same fail-closed semantics as the scc.sh self-update check.
- The user's "no" decision is cached per upstream version: once you've
said no to
1.0.135, you won't be re-prompted until npm publishes a newer one. Cache lives at~/.scc/claude-update-check.
Architectures
scc is a thin wrapper around Claude Code, so the supported arches are exactly the arches
Anthropic publishes the Claude binary for: linux/amd64 and linux/arm64 (glibc and musl
variants resolve automatically at install time).
| Flavor | linux/amd64 | linux/arm64 |
|---|---|---|
fedora/python-pip-rust-golang |
✓ | ✓ |
fedora/python-pip-openjdk-maven-springboot |
✓ | ✓ |
fedora/android-flutter-ios |
✓ | ✗ |
alpine/alpine |
✓ | ✓ |
fedora/android-flutter-iosislinux/amd64only because Google ships nolinux/arm64Android SDK channel. Apple Silicon hosts run it under Rosetta (OrbStack automatic; Docker Desktop via Settings → General → "Use Rosetta for x86_64/amd64 emulation"). Rosetta is fast enough — it's the same path Android Studio uses on macOS. CocoaPods is bundled sopod installresolves iOS dependencies inside the container; the actualflutter build ipastep still requires macOS + Xcode and must run on a Mac.
Host install (via installer/i.sh) likewise accepts macOS (Apple Silicon + Intel) and Linux
on x86_64 / aarch64. Other host arches are rejected — there's no Claude binary that
would run inside the resulting container.
Building & publishing
./build.sh # auto-version + default flavor for host arch
./build.sh -V 1.2.3 # explicit version
./build.sh -i fedora/python-pip-rust-golang # specific flavor
./build.sh -i all # every flavor
./build.sh -a linux/amd64,linux/arm64 -i all -p # multi-arch, push manifest list
./build.sh -k 5 # keep 5 old versioned tags besides :latest
./build.sh -p # push the resulting tags
./build.sh -p -r forge.example/ns/scc-other # push to a different registry/repo
./build.sh -h # full help
Default push target: forge.stacktop.network/openstacktop/scc. Run
docker login forge.stacktop.network once before the first push.
By default build.sh builds for the host arch and --loads into the local Docker daemon.
With -a listing more than one platform, the build switches to a multi-arch manifest and
must --push — Docker's local image store can't hold a manifest list. CI uses this
mode to ship the linux/amd64 + linux/arm64 manifest behind a single tag.
build.sh -i all walks images/*/*, skips symlinked aliases (so default/default
isn't built twice), and dedupes by realpath. Pruning and pushing happen once at the
end of the run, not per-build.
Running
scc # mount $PWD and ~/.scc state
scc ~/code/myproj # mount a specific dir
scc -c # workspace only, fresh Claude state (no .claude mount)
scc -w # Claude state only, /workspace empty (no workspace mount)
scc -cw # fully sandboxed: no workspace, fresh state
scc -h # full help
Pick a non-default flavor via SCC_IMAGE. Two options:
# Pinned: this exact build forever
SCC_IMAGE=scc:1.0.6-alpine scc .
# Rolling: always the latest alpine flavor
SCC_IMAGE=scc:alpine-latest scc .
Force the entrypoint to re-download Claude (cache busting):
SCC_FORCE_REINSTALL=1 scc .
Updating scc.sh
The launcher checks once a day (HTTPS, 1 s timeout) whether a newer scc.sh
is published on main. When one is, scc prints the recommended installer
command and a link to the source view, then prompts before continuing:
⚠ scc.sh update available: 1.0.0 → 1.1.0
review the new launcher first:
https://forge.stacktop.network/openstacktop/scc/src/branch/main/scc.sh
then update with:
curl -fsSL https://.../installer/i.sh | bash
⚠️ Updating runs remote code on your machine. Updating scc is on your responsibility — review the diff in the source view before pasting the installer one-liner. Never paste a curl-pipe-bash command without reading what you're executing.
The check never auto-runs the installer. To suppress the daily check entirely:
SCC_NO_UPDATE_CHECK=1 scc. To force a fresh check next run: rm ~/.scc/update-check.
Note
:
scc.shhas its own version (scc.sh.versionat the repo root) independent of the image version. The launcher only changes when the launcher itself does — most image releases (scc:1.0.5→scc:1.0.6etc.) don't bump it. So expectscc.sh.versionto lag behind the image's:latesttag, and don't worry if your launcher reports an older version than the image you're running.
Project configs
Drop a .scc/ directory inside any workspace to opt into per-project settings.
Bootstrap one with scc init (interactive), or create the files by hand —
both paths produce the same layout. Three pieces, each independent:
your-project/
└── .scc/
├── config.ini # optional — pins the image for this project
├── dot-claude/ # optional — per-project /root/.claude bind
└── dot-claude.json # optional — per-project /root/.claude.json
Bootstrap (scc init)
cd /your/project
scc init
Prompts for:
- Image — Enter to inherit the default
forge.stacktop.network/openstacktop/scc:latest, or type a specific tag. Aconfig.iniis written only when you override the default — keeps the.scc/directory minimal. Tag styles:scc:1.0.6-alpine— pin to an exact build (reproducible).scc:alpine-latest— track the latest alpine flavor (rolling).scc:latest— track the default flavor.
- Isolate Claude state? —
ycreates.scc/dot-claude/+.scc/dot-claude.jsonso login + sessions are per-project. - Copy login credentials? — only asked when isolation is selected and
~/.scc/dot-claude/.credentials.jsonexists.ycopies just the credential file (auth tokens), leaving session/conversation history fresh in the new isolated state.
Run scc init /path/to/project to bootstrap a different directory.
Schema
.scc/config.ini:
[scc]
# Pinned to a specific release — every checkout of this repo gets the
# same image bytes, regardless of when scc was run.
image = forge.stacktop.network/openstacktop/scc:1.0.6-alpine
# Default args forwarded to claude on every `scc` in this workspace.
# Same shell-style tokenizer as --ca; no $VAR / $(…) expansion.
claude_args = --model sonnet --output-format json
# Docker --platform pin. Use when the project's image only ships one arch
# and your host is the other — typically Apple Silicon (arm64) running an
# amd64-only image under Rosetta-for-Linux. Empty / omitted lets docker
# pick from the image manifest.
platform = linux/amd64
# Per-project Docker named volume for the Claude install. By default every
# scc invocation shares `scc-claude` across all flavors; pin a separate
# volume here when an image's libc / runtime layout differs enough that
# the shared binary won't run under it. The volume holds only the Claude
# binary cache (~/.scc/dot-claude still holds login state), so the extra
# disk cost is one binary per pinned volume (~236 MB).
volume = scc-claude-android-flutter
All four fields are optional and independent — pin any subset. Image rolling form, if you prefer to track the newest build of a flavor:
[scc]
image = forge.stacktop.network/openstacktop/scc:alpine-latest
Resolution priority:
- image —
SCC_IMAGEenv >.scc/config.ini> default - platform —
SCC_PLATFORMenv >.scc/config.ini> (omit, docker chooses) - volume —
SCC_CLAUDE_VOLUMEenv >.scc/config.ini>scc-claude - claude_args — always read (and prepended before any
--cafrom the CLI)
Trust prompt — the first time a project pins a non-default image, any
claude_args, OR a platform, scc asks before running it (a malicious
.scc/config.ini could otherwise route you to any registry, pin dangerous
flags, or force-emulate a different arch):
⚠ .scc/config.ini wants a non-default configuration:
workspace: /Users/you/code/myproj
image: forge.stacktop.network/openstacktop/scc:1.0.6-alpine
claude_args: --model sonnet --output-format json
platform: linux/amd64
Trust this configuration for this workspace? [y/N]
Accept once and the decision is cached at ~/.scc/trusted/<sha256(workspace)>
keyed by the (image, claude_args, platform) triple. If the project later
changes any field, you're re-prompted with an OLD → NEW diff so you can see
what changed:
⚠ .scc/config.ini for this workspace changed since last trust:
workspace: /Users/you/code/myproj
image: forge.stacktop.network/openstacktop/scc:1.0.6-alpine
claude_args: --model sonnet
→ --model sonnet --dangerously-skip-permissions
Bypass:
SCC_IMAGE=… scc— env wins for image, no prompt for image (claude_args still gated).SCC_PLATFORM=… scc— env wins for platform; no prompt for that field.SCC_ALLOW_PROJECT_CONFIG=1 scc— auto-trust this run + cache.SCC_ALLOW_PROJECT_IMAGE=1— legacy alias for the above; still accepted for one release.- Non-TTY runs (CI, pipes) fail closed — set
SCC_ALLOW_PROJECT_CONFIG=1.
Note that even an accepted trust prompt won't let project-pinned dangerous
flags through — those still need SCC_ALLOW_DANGEROUS_FLAGS=1 per invocation
(see Passing args to claude). Two independent gates.
Wipe trust: rm ~/.scc/trusted/<sha> for one workspace, or rm -rf ~/.scc/trusted for all.
Per-project Claude state — drop a .scc/dot-claude/ directory and the
launcher binds it to /root/.claude instead of the host-global
~/.scc/dot-claude. Same idea for the adjacent .scc/dot-claude.json. Login
state, conversation history, and MCP config become per-project.
The Claude binary still lives in the shared Docker volume scc-claude —
no point reinstalling per project.
Recommended .gitignore for repos using a .scc/dot-claude/:
/.scc/dot-claude/
/.scc/dot-claude.json
.scc/config.ini itself is fine to commit; the trust prompt is the safety net.
Files
| Path | Purpose |
|---|---|
scc.sh |
Launcher: resolves workspace, conditionally mounts state, runs the image |
build.sh |
Builds + tags (per-flavor and default aliases), prunes, optional push |
install.sh |
Symlinks the launcher onto PATH for local clones (macOS + Linux) |
installer/ |
One-line curl | bash installers (e.g. installer/i.sh for macOS/Linux) |
images/ |
Per-flavor Docker build contexts (Dockerfile + config.ini) |
<workspace>/.scc/ |
Optional per-project overrides (image pin, isolated Claude state) |