scsh — Scoped Skills Helper
Run your repository's agent skills in parallel — each in its own throwaway
container, on a clean clone of your repo — with one command. No Dockerfiles, no
docker run, no risk to your working tree.
That's the whole interface. scsh reads a small .scsh.yml at your repo root,
builds one container image, and runs every skill you listed at once — each one
isolated, each producing a result file that scsh copies back into your repo.
Understand this first: scsh is a single Rust binary plus a tiny per-repo config.
You describe what to run (skills, with the environment they need); scsh owns how —
the clean clone, the container, running as you, collecting results. Nothing touches your
working tree, and a real run insists on a committed, clean repo so what runs is exactly
what you committed.
To see it in action: clone this repo and cargo build --release, then — from any
empty directory that is not yet a git repository — start your favorite skills-aware
agent and ask it to follow the steps in DEMO.md. It will build a tiny
scsh project right there and run it: the add skill computes a sum (with defaults and
with values you pass), and multiply runs under its profile when given X/Y — and is
refused by scsh itself when they're missing.
What is a "skill", and why would I want this?
A skill is a folder in your repo — .skills/<name>/SKILL.md — holding
plain-English instructions for an AI coding agent (the same SKILL.md format used by
Claude Code, Cursor, opencode, …). A skill might "summarize every open TODO",
"regenerate the changelog", or anything you can write down.
Skills are easy to write. Running them well is the hard part — you want each on a
real checkout of your repo (never your actual working tree), several at once
without stepping on each other, each reproducible and disposable, and running as
you (not root). scsh is the one command that does all of that: you describe
what to run, scsh owns how.
Quick start
# 1. Build it (Rust toolchain required). Drop the binary on your PATH, or use it from
# target/release/scsh, or run it via `cargo run -- <command>`.
# 2. Inside any git repo, scaffold AND commit a demo project (config + example skills,
# /tmp gitignored, all committed — leaving a clean, ready repo).
# 3. See what would happen — no containers, no network.
# 4. Do it for real.
Need to discover or gate on profiles from another tool? Two runtime-free commands
(just git + a valid .scsh.yml, no container runtime) make it scriptable:
First time?
DEMO.mdis a guided, English walkthrough that builds a tinyscshproject from an empty directory and runs it — follow it yourself, or hand it to your AI assistant.
A real scsh run requires a clean working tree (it clones committed state into
the container), so commit or stash your changes first — init-demo-project does that
for you.
The .scsh.yml config
You describe your project and its skills; you never write a container command.
The whole file is just your skills — no version/project/image boilerplate. scsh
builds them on a built-in base image (debian:bookworm-slim, with opencode added for you).
skills: # the only top-level key — one or more, keyed by skill name
add: # matches a .skills/add/ in your repo
harness: opencode # required — the engine that runs the skill
model: openai/gpt-5.4-mini-fast # optional — the model the harness passes through
timeout: 600 # optional — seconds; kill & fail if exceeded
commits: true # optional — rebase commits the skill makes onto your branch
env: # optional — host vars to forward (scsh resolves these)
- A: ${A:-2} # forward A if set, else inject the default 2
- B: ${B:-3}
result: tmp/add_result.json # required — the repo-relative file the skill must write
multiply: # only runs under: scsh run --profile multiply
harness: opencode
profile: multiply # keeps a var-requiring skill out of the default run
env:
- X: ${X} # ${X}: required — scsh refuses the skill if X is unset
- Y: ${Y}
result: tmp/multiply_result.json
A skill's fields
harness(required) — the engine that runs the skill inside the container. Today there's one:opencode(runsopencode -m <model> run "run skill <name>"). The harness is the extension point.model(optional) — the model the harness passes to its tool.result(required) — a repo-relative path the skill must create (keep it under the gitignoredtmp/). A missing result fails the skill. When it appears,scshparses it as JSON and prints the message — aresult/messagefield, or a lone single field — on the skill's line (not just the path).profile(optional) — groups a skill under a named profile.scsh run(no--profile) runs the reserveddefaultprofile: the skills with noprofile:.scsh run --profile <name>runs only that profile's skills; pass a comma/semicolon list to run several (--profile default,<name>adds the default ones back in). If every skill is profiled, a barescsh runis an intentional no-op —scshprints the available profiles and their skills.commits(optional, defaultfalse) — whentrue, a skill may commit to its clone, andscshbrings those commits back: after the run it rebases the skill's new commits onto your current branch. Several commit-enabled skills compose (each rebases onto the branch the previous one advanced — no fast-forward assumed). If a skill's commits don't apply cleanly,scshleaves your branch alone and saves them to a distinctscsh/incoming/<skill>-<utc>-<short>branch for you to inspect and merge. Bringing commits in is a real side effect: run twice and you get the commit twice —scshnever dedups or skips it. (The image includesgit, and each clone gets a local commit identity so the skill can commit.) Those commits are authored by a deliberately unmistakable bot —dkorolev-neon-elon-bot— and are local-only by design (scshrebases, never pushes); if that author ever turns up in a code review or a pushed commit list, you pushed something you shouldn't have. Seescsh help cache.env(optional) — host variables to forward.scshresolves each value:${VAR}/$VAR— requireVAR; refuse the skill if it's unset (a bareKEY: VARis a literal — to forward a variable writeKEY: ${VAR}).${VAR:-default}— forwardVAR, or injectdefaultwhen unset (${VAR:-}= empty).${VAR:?message}— requireVAR, refusing with yourmessage.
scsh validates .scsh.yml strictly and all at once — every problem (unknown keys,
a missing skills block, a malformed env spec, a result path that escapes the repo) is
reported together, so you fix them in one pass.
Commands
scsh Show help (the default — a bare scsh is safe and never runs).
scsh run [--profile X] Preflight, then build the image & run the selected skills in parallel.
scsh list (alias: ls) List every skill by profile — result, commits, env (--verbose: + internals).
scsh init-demo-project Scaffold AND commit a demo: .scsh.yml + example skills + tmp/ ignore.
scsh installskills [url] Install skills — bundled, or a git repo's (merges its .scsh.yml).
scsh updateskills [url] Reinstall skills, overwriting files — bundled or a git repo's.
scsh help Show help (includes the schema).
scsh version Show the version (with the build's git short hash, +`-dirty`).
Installing skills. With no arguments, scsh installskills drops scsh's one bundled skill —
scsh-harness-demo-and-selftest, a demo-and-self-test you run with /scsh-harness-demo-and-selftest
— into your repo's .skills/, and points you at a real skills repo for anything else. Give it one
or more git URLs to install the skills those repos ship (installed in order, as if you ran the
command once per repo):
# several at once — installed in order, landing as one reviewable diff:
Like a real run, installskills/updateskills insist on a clean working tree (so the install
is a reviewable diff, not mixed into unrelated work) and make sure /tmp is gitignored before
writing, so the repo is run-ready afterward.
If the source repo has its own .scsh.yml, that manifest drives the install. scsh
validates it first (and stops if it's malformed), then for every skill it lists — except
the authoring-only ones (marked autoinstall: false, or named with the
internal- prefix, e.g. a repo's own self-check skill) — it copies the skill folder
and merges that skill's entry into your .scsh.yml. The skills are then runnable immediately: a default skill on scsh run, a
profiled one on scsh run --profile <name>. Skills the manifest doesn't list are skipped
(the manifest is the shipping list), and skills already in your .scsh.yml are left
untouched. Without a source .scsh.yml, scsh simply installs every .skills/<name>/
folder it finds (no manifest merge).
Either way it wires up the five host symlinks (.claude/skills, .codex/skills,
.cursor/skills, .opencode/skills, .agents/skills → ../.skills), and never clobbers
a file that differs from the source (an identical one is simply "already installed"). Use
scsh updateskills [url] to overwrite skill files with the source's version.
The legacy flags --help/-h, --version/-V, and --init-demo-project still work as
aliases. (ls is an alias for list.)
Set SCSH_RUNTIME=<docker|podman|container> to force a container runtime instead
of auto-detection (handy when the auto-picked one can't reach the system temp dir —
e.g. snap-packaged Docker, where SCSH_RUNTIME=podman is the fix; scsh already
prefers Podman over a snap Docker automatically).
Watching a run (the live board)
On a terminal, scsh run shows an interactive live board: the image build and every skill
are collapsible rows, each a ▶/▼ triangle, a status glyph (spinner → ✓/✗), the label,
a smart elapsed clock, and the latest output line.
- Press
Ctrl+1…Ctrl+9— each row is labelled[Ctrl+1],[Ctrl+2], … on the left; press a row's number to expand/collapse it. This is the reliable path when the mouse isn't forwarded: scsh turns on the terminal's keyboard-enhancement protocol soCtrl+1…Ctrl+9all work (on a terminal without it, the plain digit toggles instead). You can also click the row if your terminal forwards the mouse. - Expanding shows that process's full output beneath it, every line stamped with its time relative
to when that process started (
+1.2s). Open shows it all; closed tucks it away. - Scroll with the mouse wheel, ↑/↓, PgUp/PgDn, or Home/End. It follows the tail
until you scroll up, and resumes following at the bottom.
e/cexpand/collapse every row. Ctrl-Caborts the run (and tears the containers down).
The board is drawn inline in the normal terminal buffer — not a full-screen takeover — so
your terminal's own scrollback keeps working during the run. When the run finishes, scsh wipes
the live region and leaves a compact ✓/✗ summary in its place: one line per process, nothing
more. Off a TTY (a pipe, a file, CI) there's no board — each step prints a plain ▶ then ✓/✗
line, so logs stay readable.
See it without a container or a model:
scsh __ui-demoruns the real board over a few scripted subprocesses (click the rows, scroll), andscsh __ui-demo --framesprints a few static frames of it — handy in a doc or a pipe.
Caching
scsh caches each skill's result. Before running a skill it computes a SHA-256 over
a deterministic blob of:
- the repo's committed content (the git
HEADtree), - the skill's own files (
SKILL.md+ scripts), and - the resolved environment forwarded to the skill (sorted).
Same content + same skill + same env → same key → a cache hit: scsh restores the
result instantly and prints (cached) — no clone, no container, no model call. A miss
runs the skill and stores its result. The cache lives in the repo's gitignored
tmp/.sccache/<sha256>.json, and nowhere else.
A commit-enabled skill's run also journals the commits it made into that cache entry.
When it commits, the repo's tree changes, so the next run is a miss (it runs again). But
get back to the same committed state (e.g. git reset --hard) and re-run: it's a hit that
restores the result and replays the journaled commits — so the commit reappears on top.
A cache hit reproduces the full side effect, not just the result file. scsh help cache
has the details.
What's in the container image
scsh builds one image (a glibc Debian-slim base) shared by every skill, baked with
a broad dev/CLI toolchain so skills can do real work with no setup step:
- Languages & build:
python3(+uv), Go, Rust (cargo), C/C++ (gcc/g++,make,cmake,pkg-config,libssl-dev),perl,gawk,node(+opencode, the harness). - Data & CLI:
jq,yq,ripgrep,shellcheck,git(+git-lfs),gh(GitHub CLI),sqlite3,postgresql-client(psql),protobuf-compiler(protoc),curl/wget,tar/gzip/xz/zip/unzip,patch/diffutils,tree,less,file. - Cloud:
aws(AWS CLI v2),gcloud+gsutil,kubectl. - Networking:
ping,traceroute,dig/nslookup,nc(netcat),ss/ip,whois,socat. - Base:
ca-certificates,gnupg,openssh-client, a UTF-8 locale (C.UTF-8).
Java is intentionally not installed — nothing in scsh or the example skills is JVM, and a JDK adds ~300 MB. If you need it, that's a deliberate future add (a per-repo image override).
Timezone: the image is built with the timezone of the machine that builds it (scsh
passes the host's TZ as a build arg), so timestamps a skill produces line up with your
machine. (scsh run does the build; see scsh help internals.)
Platform-agnostic: the image builds on x86_64 and arm64 (Apple Silicon, arm
servers) alike — every architecture-specific download resolves the target arch at build time,
so there are no hardcoded-arch URLs. The Dockerfile is a single static file,
src/Dockerfile, embedded into the binary at compile time.
The base is glibc Debian (not musl Alpine) precisely so these prebuilt toolchains install and
run without friction. The image is large (a few GB) and built once, then cached and reused
across runs — the first scsh run (or any change to the Dockerfile) rebuilds it.
What you need
- A Rust toolchain (
cargo) to build the binary. giton yourPATH.- A container runtime: Apple
container→ Docker → Podman on macOS; Docker → Podman on Linux. - Network only for a real
scsh run(it pulls the base image and installs opencode).listandinit-demo-projectneed none. - For skills to do real work, the container's opencode needs a configured model;
scshforwards your host opencode login into each run for its duration.
Safety & guarantees
- Your working tree is never touched. Containers only ever see a throwaway clone
in the system temp dir; the only thing written back is each skill's
result, into the gitignoredtmp/(existing files backed up, never clobbered). - A real run refuses unless the repo is clean and
/tmpis gitignored, so scratch and results can never be committed by accident. - Least privilege. The container runs as a non-root
agentuser whose UID/GID match yours, so files it writes are owned by you. - Secrets don't linger. Your opencode credential is copied into a run only for its
duration and removed afterward (opt out with
SCSH_NO_OPENCODE_AUTH=1). - Scratch is cleaned up. Each skill's container is
--rm, and its throwaway clone in the system temp dir is removed after the skill succeeds; a failed skill's clone is kept for inspection (its path is printed), and clones older than a day are swept at the next run's start. Keep every clone withSCSH_KEEP_RUNS=1. - Nothing outward happens for you.
scshbuilds and runs locally; it never pushes, publishes, or deletes your data.
A note on
tmp/. Throughoutscsh,tmp/means the gitignoredtmp/subdirectory of your repo — never the operating system's temp dir (which we call "the system temp dir"). The/tmpline in.gitignoreis anchored to the repo root, so it matches<repo>/tmp/and has nothing to do with the OS/tmp.
Building & repo conventions
- Build: a Rust toolchain —
cargo build --release. The crate is std-only; its only dependencies arecrossterm+console(the live UI) andsignal-hook(signals). - Formatting:
rustfmt.tomlpins the house style — runcargo fmtbefore committing. Cargo.lockis committed on purpose:scshis a binary, so the lockfile pins exact dependency versions for reproducible builds.- The image installs the latest
opencodevianpm(npm install -g opencode-ai) — more reliable than the upstream curl installer, and easy to pin. - There is no root
.scsh.ymlin this repo, by design. It is the tool, not a consumer of itself, so there is nothing to run against it.scsh init-demo-project(andinstallskills) is what writes a.scsh.ymlinto a demo or target repo.
Learn more
- DEMO.md — the invocation test suite in plain English: follow it (or
ask your AI to) to watch
scshexplain every failure mode from its own output. - CONTRIBUTING.md — house style and conventions.
scsh help— the full command list and the.scsh.ymlschema.