scsh 1.9.6

Scoped Skills Helper — preflight a git repo and run its scoped skills in ephemeral containers.
scsh-1.9.6 is not a library.

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.

scsh run

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>`.
cargo build --release

# 2. Inside any git repo, scaffold AND commit a demo project (config + example skills,
#    /tmp gitignored, all committed — leaving a clean, ready repo).
scsh init-demo-project

# 3. See what would happen — no containers, no network.
scsh list                  # list every skill by profile (add --verbose for the Dockerfile + plan)

# 4. Do it for real.
scsh run                   # build the image, run every default skill in parallel

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:

scsh list --json           # {"profiles":[{"name":"default","skills":["add"]}, …]}  → pipe to jq
scsh check-profile multiply  # exit 0 iff that profile exists with ≥1 skill (else non-zero)

First time? DEMO.md is a guided, English walkthrough that builds a tiny scsh project 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 built-in harness images (scsh-opencode, scsh-claude) from a shared Debian base.

skills:                          # each key == .skills/<name>/ folder
  add:                           # direct run OR invocations: matrix (below)
    timeout: 600
    env:
      - A: ${A:-2}
      - B: ${B:-3}
    result: tmp/add_{name}_result.json   # {name} required when invocations: is set
    invocations:
      opencode-gpt-5.4-mini-fast:
        harness: opencode
        model: openai/gpt-5.4-mini-fast
        commits: true
      claude-sonnet-4-6:
        harness: claude
        model: sonnet
      opencode-glm-5.2:
        harness: opencode
        model: nebius-glm/zai-org/GLM-5.2
  multiply:
    profile: multiply
    env:
      - X: ${X}
      - Y: ${Y}
    result: tmp/multiply_{name}_result.json
    invocations:
      opencode-gpt-5.4-mini-fast:
        harness: opencode
        model: openai/gpt-5.4-mini-fast
      claude-sonnet-4-6:
        harness: claude
        model: sonnet

At run time, each invocations: route expands to an invocation named {skill}-{route} (for example add-opencode-gpt-5.4-mini-fast). A skill with direct harness: / model: fields runs as a single invocation named after its key.

A skill's fields

  • harness (required for direct run)opencode or claude. Omit at the skill level when using invocations:; each route supplies its own.
  • invocations: (optional matrix) — named routes, each with harness, optional model, optional profile (overrides the skill-level default), optional commits (overrides the skill-level default). Mutually exclusive with top-level harness / model. OpenCode runs opencode -m <model> run "run skill <source>". Claude runs headless via claude -p with the skill's SKILL.md (host CLAUDE_CODE_OAUTH_TOKEN from claude setup-token, or ~/.claude/.credentials.json, plus optional ~/.claude / ~/.claude.json mounted into the container).
  • model (optional) — the model the harness passes to its tool.
  • result (required) — a repo-relative path the skill must create (keep it under the gitignored tmp/). A missing result fails the skill. When it appears, scsh parses it as JSON and prints the message — a result/message field, 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 reserved default profile: the skills with no profile:. 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 bare scsh run is an intentional no-op — scsh prints the available profiles and their skills.
  • commits (optional, default false) — when true, a skill may commit to its clone, and scsh brings 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, scsh leaves your branch alone and saves them to a distinct scsh/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 twicescsh never dedups or skips it. (The image includes git, 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 (scsh rebases, never pushes); if that author ever turns up in a code review or a pushed commit list, you pushed something you shouldn't have. See scsh help cache.
  • env (optional) — host variables to forward. scsh resolves each value:
    • ${VAR} / $VARrequire VAR; refuse the skill if it's unset (a bare KEY: VAR is a literal — to forward a variable write KEY: ${VAR}).
    • ${VAR:-default} — forward VAR, or inject default when unset (${VAR:-} = empty).
    • ${VAR:?message}require VAR, refusing with your message.

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`).
scsh daemon start|stop|restart|status
                           Session browser on http://127.0.0.1:7274 (override with SCSH_DAEMON_PORT).
                           scsh run auto-starts an ephemeral daemon and prints a session URL.

See DAEMON.md for the session browser API and demo script.

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

scsh installskills https://github.com/dkorolev/beautiful-skills
# several at once — installed in order, landing as one reviewable diff:
scsh installskills https://github.com/dkorolev/beautiful-skills https://github.com/dimacurrentai/code-review-skills

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 YAML block verbatim into your .scsh.yml** (same schema, including invocations:matrix skills). Existing skill keys in the consumer are left untouched — scsh warns when a key would conflict. The skills are then runnable immediately: a default skill onscsh run, a profiled one on scsh run --profile . Matrix skills expand to {skill}-{route}invocations at run time. Skills the manifest doesn't list are skipped (the manifest is the shipping list), and skills already in your.scsh.ymlare left untouched. Without a source.scsh.yml, scshsimply installs every.skills//` 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 09, then AZ — image builds appear first, then skill rows in manifest order; each labelled on the left ([0], [1], …). Press the row's shortcut to expand/collapse it. This is the reliable path when the mouse isn't forwarded: scsh turns on the terminal's keyboard-enhancement protocol so Ctrl+digit works too (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/c expand/collapse every row.
  • Ctrl-C aborts the run — SIGTERM on every child and container, then SIGKILL after one second.

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.

When the session browser daemon is running, the same events also appear in a browser at a deep link http://127.0.0.1:7274/session/abcdef URL printed at the end of scsh run.

See it without a container or a model: scsh __ui-demo runs the real board over a few scripted subprocesses (click the rows, scroll), and scsh __ui-demo --frames prints 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 HEAD tree),
  • 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.
  • git on your PATH.
  • 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). list and init-demo-project need none.
  • For skills to do real work, the container's opencode needs a configured model; scsh forwards 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 bind-mounted from the host (push IN). Skills must not git fetch, git pull, or git clone inside the container. After each skill, scsh pulls OUT on the host: the result file always; new commits only when commits: true and the skill committed (local fetch from the run clone — not GitHub). scsh never pushes to any remote. in the system temp dir; the only thing written back is each skill's result, into the gitignored tmp/ (existing files backed up, never clobbered).
  • A real run refuses unless the repo is clean and /tmp is gitignored, so scratch and results can never be committed by accident.
  • Least privilege. The container runs as a non-root agent user 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 with SCSH_KEEP_RUNS=1.
  • Nothing outward happens for you. scsh builds and runs locally; it never pushes, publishes, or deletes your data.

A note on tmp/. Throughout scsh, tmp/ means the gitignored tmp/ subdirectory of your repo — never the operating system's temp dir (which we call "the system temp dir"). The /tmp line in .gitignore is 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 are crossterm + console (the live UI) and signal-hook (signals).
  • Formatting: rustfmt.toml pins the house style — run cargo fmt before committing.
  • Cargo.lock is committed on purpose: scsh is a binary, so the lockfile pins exact dependency versions for reproducible builds.
  • The image installs the latest opencode via npm (npm install -g opencode-ai) — more reliable than the upstream curl installer, and easy to pin.
  • There is no root .scsh.yml in 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 (and installskills) is what writes a .scsh.yml into 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 scsh explain every failure mode from its own output.
  • CONTRIBUTING.md — house style and conventions.
  • scsh help — the full command list and the .scsh.yml schema.