skill
stackdeck
Your projects folder, as a control panel: start, stop, branch-switch and watch your local dev services. Zero dependencies, never talks to the internet.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/beingmechon/stackdeck itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 14, 2026Your agent made four worktrees. Something has to run them.
Making the worktree is the easy part. The hour after is the problem: four branches, four dev servers, ports you didn't choose, and no idea which are up or what they're printing. Stackdeck starts them, keeps them running, streams their logs, shows every port on the machine, and hands all of it to your agent over MCP.
It works on the rest of your projects folder too — scans your roots, works out how to run each repo across ~15 ecosystems, boards them all. One Node process, one HTML page, no dependencies, bound to 127.0.0.1, never talks to the internet.
Install
npm i -g stackdeck # npm
brew install beingmechon/tap/stackdeck # Homebrew
curl -fsSL https://beingmechon.github.io/stackdeck/install.sh | sh # no npm needed
stackdeck # starts the daemon, opens http://localhost:8899
stackdeck tui # the same board, in your terminal
Point it at your projects folder in settings, then press configure on any
project. npx stackdeck runs it without installing anything.
Node 18.13+, macOS or Linux — WSL2 included, with caveats;
Windows-native is out of scope. The curl installer unpacks into ~/.local
after checking the tarball against the hash npm recorded at publish time
(read it first); undo with … | sh -s -- --uninstall.
stackdeck 5 running · 12 stopped
─────────────────────────────────────────────────────────────────────
Shop 2/3 up
● api :3001 71760 pnpm run dev
● ⧉ feature/rate-limits :3002 71874 worktree
○ studio :5173 pnpm run dev
─────────────────────────────────────────────────────────────────────
↑↓ move s start x kill r restart ⏎ logs / filter p ports o open q quit
What it does once the worktree exists
It runs them, and keeps running them. Every branch gets its own row, log
stream and branch.service.localhost address, so two branches of one API can
be open in two tabs. restart: on-failure backs off exponentially. dependsOn
starts a stack in order, waiting for each piece to be ready — a log regex, an
HTTP check, or its port opening — not merely spawned. Start on demand boots a
stopped service when you open api.localhost and holds the request until it
answers; idle stop shuts it down again, so twenty services cost nothing until
you open one.
Your agent can drive all of it. stackdeck mcp puts the board on MCP.
Starting returns only once the service is actually serving, so an agent never
guesses with a sleep loop — the one thing every integration otherwise
reimplements. Setup below.
Every port on the machine, not just yours. Which service owns it, the full
command line, its directory and parent, and a two-step kill — how you find the
dev server from three weeks ago squatting :3000. In the browser, the TUI
(p), and the terminal:
stackdeck ports 3000 # who has it, since when, and from which directory
stackdeck kill 54211 # only pids holding a port; never root, never sudo
- Worktrees that don't cost a gigabyte — ⚠️ experimental. A branch
symlinks
node_modules,target,.venvand friends back at your main checkout instead of copying, picked from the ecosystems the repo actually is. Never anything git tracks, never over something already there. It appends the linked names to.git/info/exclude— a real file in your repo, though one git never commits — because a.gitignorewrittennode_modules/does not match a symlink, and the worktree would otherwise show it as untracked. Also new, also unused by anyone but me.linkDirs: [...]overrides the detected list,falseturns it off entirely. - A branch can have its own database — ⚠️ experimental.
isolateDbcopies the dev Postgres database per worktree (CREATE DATABASE … TEMPLATE) and rewrites the URL, so an agent's migration can't corrupt your main checkout's data. Dropped when the worktree is removed, and only ever a name Stackdeck recorded creating — it will not drop a database it did not make. It creates and drops real databases, and no human has used it yet. I have driven it against a live Postgres 15 and it behaves, but that is one machine, one schema and one afternoon. Try it on something you can afford to lose first, and take a dump before you rely on it. Off unless you set it. - Removing a worktree deletes its directory — that is what
git worktree remove --forcedoes, and it is not recoverable. Stackdeck refuses outright for a worktree it did not create: it drops that one from the board and leaves it on disk, because it may be somewhere an agent is working. - Honest status — running state comes from pid and port. Services you
started elsewhere show as
external; killing something a supervisor restarts says so rather than pretending; killing the daemon leaves your services up for a restarted one to re-adopt. - Crash handling — unexpected exits get a badge and a browser notification.
- Sections you arrange — drag services (or move by keyboard) into groups, with start all / stop all, collapse and hide.
- *.localhost domains — a built-in reverse proxy, WebSockets included.
Browsers resolve
*.localhostnatively: no /etc/hosts, no PAC file. - Port conflicts, handled — if something holds the port, Start names the pid and offers to evict it.
- Ecosystem detection — a run command per project across ~15: Makefile and
just targets, npm/pnpm/yarn/bun, Deno, Python with uv/poetry/pipenv/pdm,
Django, cargo, go, zig, swift, dotnet, gradle, maven, Rails, Rack, Laravel,
composer, mix, flutter, stack, and
docker composelast. A monorepo becomes a section of services in one click. - Databases without Docker — Postgres, MySQL, MongoDB, Redis, Elasticsearch, RabbitMQ, NATS, MinIO, Temporal, ClickHouse and friends, detected on your machine and run as ordinary services with streamed logs.
- A terminal board too —
stackdeck tui: navigate, start, kill, restart, filter, stream logs in a split pane,pfor the port table. Same daemon, so the browser stays in sync. - CLI —
stackdeck status|start|stop|restart|logs|ports|kill. - Themes — calm, playful, austere, or a custom one built from sliders.
Keyboard-operable throughout, WCAG AA contrast, respects reduced motion.
Using it from an agent
MCP is an open standard, so this works with any client that speaks it. Point yours at:
{
"mcpServers": {
"stackdeck": { "command": "npx", "args": ["-y", "stackdeck", "mcp"] }
}
}
Most clients read that from a project .mcp.json or their own settings file;
several have a CLI shortcut that writes it — check your client's docs. npx
needs nothing installed, and stackdeck mcp starts the daemon itself.
Eleven tools: list_services, start_service, stop_service,
restart_service, get_logs, list_ports, whats_on_port, kill_pid,
list_worktrees, start_worktree, stop_worktree.
Agent instructions. Tool schemas say what an agent can call; they can't
say that start_service already waits, so a sleep loop after it is wrong, or
that it must not delete a worktree another agent is working in.
skills/stackdeck/SKILL.md says those things. It
is in the SKILL.md format some agents load automatically —
cp -r "$(npm root -g)/stackdeck/skills/stackdeck" <your-skills-dir>/ — and
anything else can read it as plain Markdown.
{
"mcpServers": {
"stackdeck": {
"command": "npx", "args": ["-y", "stackdeck", "mcp"],
"env": { "STACKDECK_PORT": "9000", "STACKDECK_HOME": "~/.config/stackdeck" }
}
}
}
It never talks to the internet
No telemetry, analytics, update checks, accounts or CDN. The page loads no external fonts, scripts or images — icon and logo are inline. The daemon listens on 127.0.0.1 and the only connections it opens are to loopback ports of services you configured. It all works on a plane. Check it while it's running:
lsof -nP -a -p "$(lsof -ti tcp:8899 -sTCP:LISTEN)" -i
# only 127.0.0.1:<port> (LISTEN), plus loopback connections to your services
Ask by the port, not by name — pgrep -f server.js also matches your editor's
helpers. Without lsof: ss -tnp | grep "pid=$(pgrep -f 'node.*server\.js')".
The one exception is a URL you type: readyWhen: { "http": "…" } fetches
exactly that address, normally your own http://localhost:….
Reference
~/.config/stackdeck/config.json (or $STACKDECK_HOME/config.json); logs sit
next to it.
{
"port": 8899,
"projectRoots": ["~/Projects", "~/work"], // folders to scan
"categoryOrder": ["Products", "Tools"], // order of project categories
"groups": ["Stack A"], // your service sections
"theme": "playful", // or austere / custom
// Set from the UI, listed so they are discoverable rather than folklore:
"excludes": ["~/Projects/scratch"], // folders the scan skips
"projectCategories": { "web": "Products" }, // project name -> category
"hiddenGroups": [], "hiddenCategories": [], // collapsed out of the board
"themeTokens": { "hue": 75, "radius": 7 }, // only when theme is "custom"
"services": [
{
"name": "api",
"dir": "~/Projects/my-app",
"command": "pnpm run dev",
"port": 3001, // status detection + api.localhost
"env": { "DEBUG": "1" },
"group": "Stack A",
"dependsOn": ["db"], // started (and ready) first
"readyWhen": { "log": "Listening on" }, // or { "http": "…" }; default: port opens
"linkDirs": ["node_modules"], // EXPERIMENTAL — symlinks into a
// worktree instead of copying;
// omit to detect, false to disable
"isolateDb": true, // EXPERIMENTAL — creates and drops
// real Postgres databases; see
// the feature list before using
"envFile": ".env.local", // env file to auto-load; false for none
"hidden": true, // keep it off the board
"restart": "on-failure", // exponential backoff, 5 tries
"onDemand": true, // boot when api.localhost is visited
"idleAfter": 30 // stop again after 30 idle minutes
}
]
}
Nothing leaves your machine, but the daemon runs shell commands on it, so the local surface matters.
localhost is not a security boundary, so the API is gated by a per-install
secret (<state dir>/secret, mode 0600). The page gets the token by being
served from disk by the daemon; the CLI reads it as the same user. Every
/api/* call without it gets a 401 — only /api/ping is open. State files are
0600 in a 0700 directory.
Layered on top: binds 127.0.0.1 only, pins the Host header (DNS-rebinding
defence), accepts only application/json POSTs (a cross-origin JSON POST
forces a preflight that is never answered; text/plain sneak-POSTs are
rejected), rejects foreign Origin headers, validates names, ports, dirs and
env server-side, caps bodies at 1MB, limits the folder browser to your home and
configured roots. Config writes are atomic; a corrupt config is set aside,
never fatal. Branch names reach git after --end-of-options, so one shaped
like a flag cannot become one. Do not port-forward the daemon off your
machine.
Known limitation, stated plainly: the token is substituted into the HTML when the daemon serves the page, so it is in the DOM. Any browser extension with host access to localhost can read it, and would then have the same API access you do. The alternatives — an httpOnly cookie, or a second fetch — each trade this for a different problem on a tool serving one page to one user, so this is accepted rather than solved.
The *.localhost proxy is unauthenticated by design — your browser needs it —
and forwards only to ports of services you configured. That makes those
reachable at guessable names from any site you visit (same-origin still blocks
reading responses), roughly the exposure a guessable port already had. It is
also why start on demand is opt-in: with it on, any page you visit could
start that service.
- Services spawn through a non-login shell using the PATH of your login shell, resolved once at daemon start — so nvm/homebrew/uv tools are found even when the daemon was launched from the GUI.
- A service that logs "Ready" but shows "this page isn't working" is almost
always the localhost cookie jar: browsers hoard cookies for
localhostacross every dev app you've run, and Node's 16KB header limit rejects the request before your code sees it (curlsucceeds because it sends none). Set{"NODE_OPTIONS": "--max-http-header-size=131072"}in the service's env, or clear cookies for localhost. $PORTis a request, not a guarantee. Vite readsvite.config, Next reads-p. If a worktree instance ignores the injected port, the board tells you where it actually bound. Make it obey with--port ${PORT:-5173}(Vite) or-p ${PORT:-3000}(Next).- Kill stops the whole process group; stragglers get SIGKILL after 5s.
- Unix only (bash, process groups, lsof/ss). The
.envloader is deliberately minimal: flatKEY=value, quotes and#comments, no interpolation, no multiline values. - WSL2: expected to work, not yet confirmed by anyone running it. WSL2 is
Linux and every path needing a macOS tool already falls back —
lsofandnetstatare usually absent there,sscovers both. WSL forwards 127.0.0.1 binds to Windows, so the board should be atlocalhost:8899in your Windows browser. Two caveats:stackdeckopens the browser viawslvieworexplorer.exeand prints the URL if neither exists, and*.localhostproxying is untested under WSL2 — binding:80needs root inside the distro, so it falls back to:8880. Reports welcome either way. - macOS app:
./scripts/macos-app.shbuilds~/Applications/Stackdeck.app. Linux: runstackdeck, or add a systemd user unit fornode server.js.
Debugging
Stackdeck swallows a lot of errors on purpose — a missing .env, an lsof
that isn't installed, a pid that died between two lines. STACKDECK_DEBUG=1
prints every one with the operation that swallowed it:
STACKDECK_DEBUG=1 stackdeck daemon
# [stackdeck:which:postgres] Command failed: bash -c command -v postgres
# [stackdeck:parseEnvFile:/home/you/api/.env] ENOENT: no such file or directory
Same behaviour, same exit codes, just louder. Put this in any bug report — it is the most useful thing for working out what happened on a machine that isn't mine.
How this relates to other tools
Creating an isolated worktree is table stakes now. workz does it as a
Rust CLI: symlinks node_modules, copies your .env, installs dependencies,
and with --isolated hands out unique ports, database names and a
COMPOSE_PROJECT_NAME. Then it exits, which is right for a setup tool.
Conductor, Claude Squad and Agent Deck work the same problem from other angles;
check each for yourself rather than taking my summary of it.
Stackdeck is the half after setup: it starts what is in the worktree, supervises it, restarts it when it crashes, streams its logs, says which ports are taken machine-wide, and exposes that to an agent over MCP. A different job, not a better one.
They compose. Stackdeck finds worktrees through git worktree list, so it
adopts whatever created them, wherever they are. Provision with
workz --isolated, run the result with Stackdeck; neither needs to know the
other exists. Its heavy-directory symlinking overlaps with what workz does — if
workz got there first, Stackdeck leaves what it finds alone.
Further back, a spiritual successor to
hotel, which did the *.localhost proxy
well and has been unmaintained since 2019. Also nearby: Foreman and Overmind
(one Procfile, no UI), Docker Compose (containers, not your host), Laravel Herd
and DBngin (excellent, macOS and PHP-shaped).
Status
Early — ten days old, one maintainer, version numbers moving fast. A solid daily driver, but treat the shape as still settling before 1.0.
server.js and index.html are two files you can read in an evening, which is
the point for something that runs shell commands. The parsers and detectors
have a test suite (test/) running on Ubuntu and macOS across Node
18, 20 and 22; the UI and the process-management path do not, and get exercised
by use. See CONTRIBUTING.md and
CHANGELOG.md.
License
MIT