The mental model behind Steckling: a git worktree + an isolated Docker service stack per branch. Each branch gets its own folder, its own services, its own data, on its own ports. Nothing collides.
A folder per branch. Via git worktree, each branch lives in its own directory. Switching branches is switching folders.
Private services per branch. A separate docker compose project per branch — own containers, own volumes, a free host port Steckling picks for you.
Env injection. Steckling writes the right DATABASE_URL (etc.) into a gitignored file your app reads, then runs your normal dev command.
It's stack-agnostic: your app runs natively the way it always has (npm run dev, rails s, go run, …). Steckling only ever deals with git, Docker, and environment variables. It never runs your app for you and knows nothing about your framework. It works for any language that reads its config from the environment.
Not a process manager. It runs your one app.run command in the foreground; it doesn't supervise multiple processes. Run workers/extra processes via a second terminal or steck exec. (steck cockpit doesn't change this — the TUI is delegated to mprocs, and its service panes are log viewports over Docker-managed containers.)
Downloads a binary to ~/.local/bin:
Homebrew treats a binary-only formula as a source build, so it runs its compiler-toolchain check even though nothing gets compiled — on a fresh macOS it can refuse with “Xcode too outdated”. If it does, update the Command Line Tools (xcode-select --install) or just use the install script above.
The engine is TypeScript on Bun:
To get a real steck command on your PATH from a source checkout, compile it with the project's build script (its flags matter — it disables Bun's automatic .env loading, which would otherwise leak worktree .env files into every app steckling runs):
mprocs — only needed for the steck cockpit TUI:
Railway CLI — only needed for steck deploy
Run steck init and the wizard writes both files (plus the .gitignore entry) for you — presets for Postgres, MySQL, Redis, Mongo, and RabbitMQ, and your detected run command. Or add the two files by hand — a services compose file and a steckling.yml.
version: 1 services: compose: ./compose.steckling.yml expose: postgres: container: 5432 env: DATABASE_URL url: "postgres://app:app@localhost:{port}/app" app: run: "npm run dev" # any language hooks: provision: "npm run migrate && npm run seed"
services: postgres: image: postgres:16 environment: POSTGRES_USER: app POSTGRES_PASSWORD: app POSTGRES_DB: app # Steckling injects the host port ports: ["${STECKLING_PORT_POSTGRES}:5432"] volumes: [pgdata:/var/lib/postgresql/data] volumes: { pgdata: {} }
$ steck up # start this branch: services + provision + run the app $ steck new feature/x # a second branch, in its own folder… $ cd ../myapp-trees/feature/x $ steck up # …with its own DB, running in parallel $ steck list # see every worktree, its status + ports
Prefer a full-terminal view? steck cockpit does the same as up but opens a TUI — your app in one pane, live service logs in the others — and quitting it parks the whole stack (needs mprocs; see commands).
Everything the steck CLI can do.
steck initSet up a repo interactively — service presets, detected run command, both config files written for yousteck new <branch> [base]Create a worktree + allocate its service ports; --ticket <id> records a ticket ID explicitlysteck up [--no-run]Bring up services, provision on first boot, run the app (--reprovision: run the provision hook again)steck cockpit [--keep-up]Cockpit TUI: app pane + live service logs, per branch (via mprocs). Quitting stops the branch's services (data kept); --keep-up leaves them runningsteck downStop the containers, keep the datasteck list / statusWhat's registered, running, and on which portssteck exec -- <cmd>Run a command wired to this branch's env (e.g. sh -c 'psql "$DATABASE_URL"')steck rm / pruneReclaim a branch's stack / bulk-reclaim merged branches — the teardown hook runs first (--purge: folder + branch too)steck deploy [--dry-run]Ship this branch's agent to Railwaysteck logs / destroyTail or tear down the deployed agentsteck mcpRun the MCP server so Claude can drive the fleetsteck doctor / configCheck the environment / validate steckling.ymlThe single config file Steckling reads from your repo root. Validate it any time with steck config.
version: 1 services: compose: ./compose.steckling.yml expose: postgres: container: 5432 env: DATABASE_URL url: "postgres://app:app@localhost:{port}/app" app: run: "npm run dev" hooks: provision: "npm run migrate && npm run seed" postCreate: "" # optional; runs after `steck new` creates the worktree teardown: "" # optional; runs before `steck rm`/`steck prune` destroy the stack ticket: # optional — parse a ticket ID out of the branch name pattern: "eng-\\d+" url: "https://linear.app/acme/issue/{ticket}" env: STECKLING_TICKET
versionConfig schema version. Currently 1.services.composePath to the compose file describing this repo's services. Run per-branch as an isolated compose project.services.exposePer service: the container port to map, the env variable to write, and a url template — {port} is replaced with the branch's allocated host port.app.runYour normal dev command. Steckling runs it with the branch's env injected — any language.hooks.provisionRuns once on a branch's first steck up — migrations, seeds, anything that prepares fresh services. Run it again with steck up --reprovision.hooks.postCreateOptional. Runs in the new worktree after steck new (identity env only — no services yet). A failure warns; the worktree is kept.hooks.teardownOptional. Runs before steck rm / steck prune destroy a stack. A failure aborts rm (unless --force) and skips that branch in prune.ticketOptional, opt-in ticket identity. Steckling parses the ticket ID out of the branch name (pattern, a regex matched case-insensitively), remembers it, shows it in steck list, and injects it into hooks and the app as $STECKLING_TICKET (name configurable via env). url is a link template — must contain {ticket} — rendered into STECKLING_TICKET_URL and steck status. It never calls the tracker's API.steck mcp runs an MCP server that exposes the fleet to AI agents. Because branches are fully isolated, you can point a separate Claude session at each one and let them work in parallel.
steckling_newCreate a worktree + stack for a branchsteckling_upBring a branch's services up and provisionsteckling_listInspect every registered worktree, status + portssteckling://registryA live resource — the registry of all branches, as MCP context. Each worktree's ticket ID rides along when a ticket block is configuredEach agent session gets a branch whose database, ports, and data belong to it alone. Agents can migrate, seed, and break things freely — nothing they do leaks into your branch or another agent's. When a branch is merged, steck prune reclaims the whole stack; add --purge to remove the worktree folder and branch too.