skill
standup-board
Presence board for Claude Code agents across machines — advisory collision-avoidance, not orchestration. GitHub-authenticated, in-memory, no database.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/kilotronic/standup-board itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 27, 2026standup·board
A presence board for Claude Code and Cursor agents: who's working on what, where, right now — across machines that don't share a memory store.
How this differs
standup is deliberately small and narrow in scope:
- Presence, not orchestration. It publishes a narrative (goal, current step, active branch/PR, worktrees) so agents and humans can see what's happening. It does not schedule work, dispatch tasks, or run agents for you.
- Advisory, not locks. Nothing here prevents two sessions from touching the same repo — it just makes that visible so an agent (or you) can decide to wait, coordinate, or proceed anyway. Compare this to swarm-protocol-style tools, which enforce cross-session locking; standup never blocks a session.
- A board, not a mailbox. Sessions post structured facts to a shared, per-owner roster that anyone's client can read — there's no addressed messaging between agents, no inbox, no conversation. Compare this to teammcp-style tools, which route MCP messages between named participants; standup has no notion of "send this to session X."
- Ephemeral and cross-machine. State lives in a SQLite file on one server
process, keyed by GitHub identity, and expires on its own (
STANDUP_TTL_SECONDS, default 12h; the web page only shows sessions seen in the last 4h). The file is ephemeral unless you mount a volume at/var/lib/standup— do that and the board survives a restart; without it, sessions simply rebuild from agents' next heartbeats. Either way it exists so a laptop and a desktop — or two worktrees of the same repo — can see each other, not to be a system of record.
Quickstart — self-host (docker-compose)
-
Create a GitHub OAuth App at https://github.com/settings/developers → New OAuth App. Set the Authorization callback URL to
http://<your-host>:8080/auth/callback(or leave it unset for a pure-LAN, device-flow-only deploy — see Config reference). Check "Enable Device Flow" —standup loginandstandup initneed it. -
Set the required variables and start the service:
SECRET_KEY=$(openssl rand -base64 48) \ GITHUB_CLIENT_ID=<from the OAuth App> \ GITHUB_CLIENT_SECRET=<from the OAuth App> \ docker compose updocker-compose.ymlbuilds the image from the repo'sDockerfile, maps port8080(override withPORT), and runs a single replica — one SQLite writer per process, so don't scale this beyond one instance. It mounts a namedstandup-datavolume at/var/lib/standup, so the board survivesdocker compose down/up; drop that volume for a purely ephemeral deploy.COOKIE_SECUREdefaults to0in compose for local/LAN http; set it to1if you put TLS in front of it. -
Visit
http://<your-host>:8080and sign in with GitHub to see the board, then runstandup login(below) to authenticate your CLI over the device flow.
Quickstart — cloud (Railway)
railway.json pins the build to the repo's Dockerfile and points the
healthcheck at /healthz; railway-template.json documents the required
service variables. To deploy:
- In the Railway dashboard: New Project → Deploy from GitHub repo, and
pick your fork of
standup-board. - Set the service variables from
railway-template.json:SECRET_KEY(generate a random 32+ byte value),GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET, and optionallyGITHUB_ALLOWED_USERS. - Generate a domain for the service, then create the GitHub OAuth App (see
below) with its callback set to
https://<your-domain>/auth/callback— or leaveOAUTH_REDIRECT_URLunset and it defaults to that same URL. - Attach a volume mounted at
/var/lib/standupso the board survives redeploys. Skip it and each redeploy starts empty, rebuilding from agents' next heartbeats. - Deploy.
GET /healthzgates the rollout.
(Publishing this as a one-click "Deploy on Railway" template button is a
follow-up — for now, railway-template.json is the reference for the
variables to set by hand.)
Client
The client is a single stdlib-only script: client/standup. There's no
manual install step — the first time you run it (by path), login and init
symlink it into ~/.local/bin/standup for you, so standup is on your PATH
afterward. (Make sure ~/.local/bin is on your PATH; the CLI warns if it
isn't. Self-install never clobbers an existing ~/.local/bin/standup — if one
is already there it's left as-is.)
1. Log in once per machine, against your board's URL. Run it by path this first time; the symlink doesn't exist yet:
./client/standup login --url https://your-board.example.com
This runs GitHub's device flow (opens a browser, asks you to enter a code),
exchanges the resulting GitHub token for a standup client token, writes both
to ~/.config/standup/env, and installs the standup symlink. No copy-pasting
a token from the web page required — standup login is the only way to obtain
a token.
2. Wire up each repo you want on the board:
cd your-repo
standup init # local: Claude hooks in .claude/settings.local.json,
# Cursor hooks in .cursor/hooks.json
standup init --shared # team: Claude hooks in .claude/settings.json,
# Cursor hooks in .cursor/hooks.json — committed
standup init --global # machine-wide: ~/.claude/settings.json +
# ~/.cursor/hooks.json,
# skill in ~/.claude/skills/standup —
# presence in EVERY repo on this machine. Wires even
# before `standup login` (inert until configured).
Either way, init vendors .claude/skills/standup/SKILL.md (always
committed — it travels with the repo so any agent that clones it gets the
behavior) and wires session start/end hooks to standup register/standup deregister for both Claude Code and Cursor. There is
nothing else to install: the CLI is the one stdlib script.
Use plain init (the default) unless the whole team runs standup — local
placement keeps .claude/settings.local.json as your personal setup: gitignored, per-machine, and never imposed on
teammates who haven't opted in. --shared commits the wiring for everyone
instead.
3. Post your narrative as work progresses:
standup status --goal 'ship the timer fix' --step 'writing tests'
4. Check who else is active before a risky operation like a rebase or arming auto-merge:
standup list # current repo
standup list --all # every repo
When a session finishes but stays open, standup done --step '<what>' keeps it
listed but stops counting it as an active co-worker; any later prompt or status post
makes it active again.
Before starting a shared work item, standup claim '<item>' posts
CLAIMED: <item> as your step, or exits 1 naming the session that already
holds it.
When a session stops on a usage limit, the StopFailure hook init wires runs
standup park. It leaves a parked row that another session, on any machine or account,
can take over with standup resume <id>. Parked rows last a week
(STANDUP_PARKED_TTL_SECONDS) and vanish when their session works again. Design:
docs/superpowers/specs/2026-11-02-park-and-resume-design.md.
All of this is fail-safe: a down or unreachable board never blocks
register/deregister/status — they just no-op.
Config reference
Client (~/.config/standup/env, KEY=VALUE lines — standup login writes
this file for you; environment variables override it):
STANDUP_URL— the board's base URL.STANDUP_TOKEN— your personal client token (standup loginwrites this).
Server:
SECRET_KEY— signs cookie sessions and client tokens (required). Rotating it invalidates every issued token — the revocation lever.GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET— from a GitHub OAuth App (required).OAUTH_REDIRECT_URL— OAuth callback URL; defaults to<host>/auth/callback. Optional — device-flow login needs no callback, so a pure-LAN self-host with no public URL still works; it only matters if you use the web page's "Sign in with GitHub" fallback.GITHUB_ALLOWED_USERS— optional comma/space-separated GitHub usernames allowed to sign in. Empty/unset = anyone with a verified GitHub email. Use it to keep a public deployment to yourself or your team.STANDUP_TTL_SECONDS— crash-safety expiry for a session; default 43200 (12h).STANDUP_PARKED_TTL_SECONDS— expiry for a parked row; default 604800 (7 days).STANDUP_DB_PATH— path to the SQLite presence DB. The container image sets it to/var/lib/standup/standup.db; mount a volume there to survive restarts. Unset (e.g. running from source) means an in-memory, ephemeral board.COOKIE_SECURE—0to allow cookies over http for local/LAN dev; default secure.
GitHub OAuth App (one-time)
Create an OAuth App at https://github.com/settings/developers:
- Authorization callback URL —
<service-url>/auth/callback(or leave unset for a device-flow-only, no-public-URL deploy). - Enable Device Flow — check this box. Without it,
standup loginandstandup init(which requires login first) cannot complete.
Put the resulting client ID/secret in your server's GITHUB_CLIENT_ID /
GITHUB_CLIENT_SECRET. Full walkthrough: docs/github-oauth-setup.md.
API
All /sessions routes require Authorization: Bearer <client-token>; the
owner is derived from the token, so a request only ever sees or mutates that
owner's sessions.
POST /sessions— field-merge upsert. Body{session_id, machine, repo}(machine/reporequired only when creating), plus any of the optional narrative/facts fields:active_branch?,last_prompt?,goal?,current_step?,active_pr?,worktrees?. Only supplied keys overwrite; others are preserved.parent_session_id?marks the row as a working subagent of another session (see below); it must not equalsession_id.DELETE /sessions/<session_id>— deregister (idempotent, scoped to you). Cascades to that session's subagent rows: the parent'sSessionEndis the only cleanup they can get, since a subagent has no hook of its own.GET /sessions[?repo=NAME]— list your live sessions.POST /sessions/<session_id>/take— remove and return onetype=parkedrow; 404 if it is gone (someone else took it) or isn't parked. A parked row'shandoffobject carriessession_id,reason,path,dirtyandunpushed.
Subagent rows
A subagent's shell inherits the parent's $CLAUDE_CODE_SESSION_ID, so a bare
standup status from one posts to the parent's row and overwrites its
narrative — and several workers overwrite each other. The SubagentStart/Stop
hooks give every subagent a nested label for free, which is enough for a
read-only Explore.
A subagent that holds its own worktree, branch and PRs is a coordination peer, so it wants a row. Nothing exposes an agent's own id to its shell, so the PARENT chooses a label and passes one flag in the prompt it dispatches:
standup status --as-subagent '<label>' --goal '...' --step '...'
--as-subagent derives <parent>:<label> and parent_session_id from the
session id already in the subagent's environment, so no id has to be pasted
into a prompt — the step most likely to be forgotten.
standup list prints those indented under their parent (↳). A child whose
parent is absent from the listing still prints at top level — hiding live work
is worse than an ungrouped row.
GET /healthz— unauthenticated health check.GET /config— unauthenticated; returns{"github_client_id": ...}so the client can start the device flow without a copy of the client ID.POST /auth/exchange— unauthenticated; body{"github_token": ...}(a GitHub access token from the device flow). Verifies it, enforcesGITHUB_ALLOWED_USERSif set, and returns{"token", "login", "email"}— the standup client token to save.GET /,GET /auth/login,GET /auth/callback,GET /auth/logout— web page + browser OAuth (cookie session, separate from client tokens).
Run locally
uv sync
SECRET_KEY=dev GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=... COOKIE_SECURE=0 \
uv run flask --app standup_board.app run
(For a local OAuth App, set the callback to
http://localhost:5000/auth/callback.)
License
MIT — see LICENSE.