Skip to content

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:

Terminal window
mkdir -p .spwn/hooks/session-created.d
cp examples/hooks/cookbook/copy-secrets.sh .spwn/hooks/session-created.d/20-secrets.sh
chmod +x .spwn/hooks/session-created.d/20-secrets.sh
git 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.

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.

Terminal window
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"
fi

Event: 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.

pull-base-branch.sh

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.

Terminal window
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"; fi
done

Rather 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.

copy-secrets.sh

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.

Terminal window
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 & disown
echo $! > "$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.

preview-env.sh

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.

Terminal window
db="session_${SPWN_TERMINAL_ID}"
createdb "$db" 2>/dev/null || true
DATABASE_URL="postgres://localhost/$db" npm run migrate
DATABASE_URL="postgres://localhost/$db" npm run seed

Terminal 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.

seed-db.sh

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.

Terminal window
# Yes/No: no options means a confirm.
if [ "$("$SPWN_BIN" prompt --header setup 'Seed the database for this session?')" = Yes ]; then
./scripts/seed.sh
fi
# 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) : ;;
esac
else
echo "no answer; starting nothing"
fi

Always 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.

confirm-setup.sh

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.

Terminal window
run_dir="$SPWN_WORKTREE/.spwn/run"
if [ -f "$run_dir/preview.pid" ]; then
kill "$(cat "$run_dir/preview.pid")" 2>/dev/null || true
fi
# docker compose -p "$SPWN_TERMINAL_ID" down -v || true
# dropdb "session_${SPWN_TERMINAL_ID}" 2>/dev/null || true
rm -rf "$run_dir" 2>/dev/null || true

Guard 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.

teardown.sh

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.