Cookbook
Each recipe here is a working script in
examples/hooks/cookbook/.
They’re templates: safe no-op defaults with the real commands commented in, for you to swap
for your stack.
To install one, copy it into a numbered file in the event’s folder, make it executable, and commit it:
mkdir -p .spwn/hooks/session-created.dcp examples/hooks/cookbook/copy-secrets.sh .spwn/hooks/session-created.d/20-secrets.shchmod +x .spwn/hooks/session-created.d/20-secrets.shgit add .spwn/hooks && git commit -m "Copy secrets into new sessions"Several recipes for the same event sit side by side as 20-…, 30-…, and run in that
order. See How hooks work for the rules.
Pull the base branch
Section titled “Pull the base branch”Use it when sessions keep starting from a stale main. It fast-forwards the base
branch in your main checkout before the session’s worktree is cut from it, so every new
top-level session starts current. Forks are skipped: their base is a parent’s spwn/…
branch, and they should inherit that tree as it is.
base="${SPWN_BASE_BRANCH:-}"case "$base" in spwn/*|cm/*|"") echo "child session; skipping"; exit 0 ;;esac
cd "$SPWN_PROJECT_DIR"git fetch origin "$base"
if [ "$(git rev-parse --abbrev-ref HEAD)" = "$base" ]; then git merge --ff-only "origin/$base"else git fetch origin "$base:$base" || echo "could not fast-forward local '$base'; skipping"fiEvent: session-created. Put it in: ~/.spwn/hooks/session-created.d/05-pull-base.sh.
It has to run before spwn’s 10-worktree.sh, which only the global folder can do: repo
hooks run after the worktree already exists, so there it only freshens the base for the
next session. As a global hook it runs for every project, and it fails harmlessly (a red
dot, nothing more) in a repo with no origin.
Copy gitignored secrets
Section titled “Copy gitignored secrets”Use it when your app needs .env or other untracked files to run. A worktree is a fresh
checkout, so anything gitignored isn’t there. This copies it across from your main checkout,
without overwriting a copy that’s already present.
files=(".env" ".env.local" ".envrc")
for f in "${files[@]}"; do src="$SPWN_PROJECT_DIR/$f" dst="$SPWN_WORKTREE/$f" if [ -e "$dst" ]; then continue; fi if [ -e "$src" ]; then cp "$src" "$dst" && echo "copied $f"; fidoneRather copy nothing in plaintext? Fetch from your secrets manager instead:
op read "op://vault/app/.env" > .env.
Event: session-created. Put it in: .spwn/hooks/session-created.d/20-secrets.sh.
Per-session preview environment
Section titled “Per-session preview environment”Use it when you want each session’s app running so you can look at it. It picks a port
from the session’s id (stable across re-runs, different per session), starts the dev
server in the background, and writes the pid and URL under .spwn/run/ for
teardown to find.
run_dir="$SPWN_WORKTREE/.spwn/run"mkdir -p "$run_dir"
hash="$(printf '%s' "$SPWN_TERMINAL_ID" | cksum | cut -d' ' -f1)"port=$(( 3000 + (hash % 1000) ))
[ -d node_modules ] || npm ci
PORT="$port" npm run dev >"$run_dir/preview.log" 2>&1 & disownecho $! > "$run_dir/preview.pid"printf 'http://localhost:%s\n' "$port" > "$run_dir/preview.url"echo "preview -> http://localhost:$port"The & disown matters. Hooks are synchronous with no timeout, so a server started in the
foreground would hold the session up forever. The node_modules check skips the install
when spwn has already cloned it into the worktree.
Event: session-created. Put it in: .spwn/hooks/session-created.d/30-preview.sh.
Add .spwn/run/ to your .gitignore. spwn commits everything in the worktree after each
turn, and you don’t want pid files on the branch.
Seed a database
Section titled “Seed a database”Use it when sessions need a database of their own to work against. It names the database after the session so parallel sessions don’t clobber each other’s rows, then migrates and seeds it. Keep every step idempotent, so clicking Run in the Hooks panel again is harmless.
db="session_${SPWN_TERMINAL_ID}"
createdb "$db" 2>/dev/null || trueDATABASE_URL="postgres://localhost/$db" npm run migrateDATABASE_URL="postgres://localhost/$db" npm run seedTerminal ids contain dashes, which Postgres identifiers don’t like unquoted. Sanitise the
name if your tools complain: tr -cd '[:alnum:]'.
Event: session-created. Put it in: .spwn/hooks/session-created.d/40-seed-db.sh.
If you need the agent’s session id, use session-ready instead: SPWN_SESSION_ID isn’t
set yet during session-created.
Prompt before setup
Section titled “Prompt before setup”Use it when some setup is expensive or optional, and you’d rather decide per session.
spwn prompt shows a picker in the
spwn UI, blocks until you answer, and prints your choice.
# Yes/No: no options means a confirm.if [ "$("$SPWN_BIN" prompt --header setup 'Seed the database for this session?')" = Yes ]; then ./scripts/seed.shfi
# Pick one. The `if` catches a decline (exit 2).if profile="$("$SPWN_BIN" prompt --header env 'Which services to start?' none web 'web+worker')"; then case "$profile" in web) my-dev-server & disown ;; web+worker) my-dev-server & disown; my-worker & disown ;; none) : ;; esacelse echo "no answer; starting nothing"fiAlways handle the no-answer branch. A scheduled run has nobody to ask and declines at once, and an unanswered prompt declines after five minutes.
Event: session-created. Put it in: .spwn/hooks/session-created.d/15-confirm.sh,
numbered before the steps it gates, or fold the prompt into those steps.
Tear down on delete
Section titled “Tear down on delete”Use it when a session-created hook started something that outlives the session. This
is the other half of the preview recipe: it stops the
server by its pid file, then clears out the run folder. It runs while the worktree still
exists, so it can read those files.
run_dir="$SPWN_WORKTREE/.spwn/run"
if [ -f "$run_dir/preview.pid" ]; then kill "$(cat "$run_dir/preview.pid")" 2>/dev/null || truefi
# docker compose -p "$SPWN_TERMINAL_ID" down -v || true# dropdb "session_${SPWN_TERMINAL_ID}" 2>/dev/null || truerm -rf "$run_dir" 2>/dev/null || trueGuard every step with || true and keep it safe to run twice. A failing teardown doesn’t
stop the delete, but it can leave things running.
Event: session-deleted. Put it in: .spwn/hooks/session-deleted.d/10-teardown.sh.
Repo session-deleted hooks run before spwn’s global 90-worktree.sh removes the worktree.
Bigger recipes
Section titled “Bigger recipes”Running the whole session inside its own container or pod takes an image and a pair of hooks, so those live in their own folders. See Per-session dev environments.