chore(agents): add a session-start hook for cloud sessions

A fresh Claude Code on the web container starts in a state where three
things are slow or wrong, all of which cost real work this week.

`bd` is not installed, so the existing `bd prime` SessionStart hook and
every bd command in AGENTS.md silently do nothing.

`.beads/issues.jsonl` is a stale export, and an agent with no database
reads it as if it were the tracker. It showed 121 issues against the
remote's 168, with all four P1 bugs OPEN though every one was fixed and
closed. Two beads were implemented from scratch on the strength of it;
both were already closed on the remote and already implemented on a
branch that had not yet merged.

The Rust workspace is uncompiled, so the first `cargo test` pays a cold
build before reporting anything.

The hook installs bd, syncs the Dolt database from the git remote, warms
the build, and prints the handful of environment facts that are not
discoverable from the repo.

Syncing needs care, because `bd init` in this repo rewrites AGENTS.md,
CLAUDE.md and .claude/settings.json, installs Cursor/Codex files, and
commits the lot. `--skip-agents --skip-hooks` stops all of that but it
still commits a .gitignore change, so the hook runs it in a throwaway
directory and copies only .beads/embeddeddolt back. Nothing it does
touches a tracked file, stages anything, or creates a commit.

Also adds a permissions allowlist for the read-mostly cargo, bd and git
commands an agent runs constantly, and context/cloud-agents.md for the
detail behind the hook — including the bd sharp edges worth knowing:
`bd dolt push <remote>` ignores its argument and pushes to origin, and
`bd dolt pull` into a divergent database refuses to merge.

The hook exits immediately unless CLAUDE_CODE_REMOTE=true, so local
checkouts are unaffected. It is idempotent: a second run is a no-op
except for the build warm, and re-running costs 0s.

Validated by running it against a clean clone: tracker syncs to 169
issues, .beads/config.yaml is left alone, no commit is created, and
`git status` shows nothing beyond the hook's own files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
This commit is contained in:
Claude
2026-09-19 06:15:01 +00:00
parent e17f14363c
commit 9ada03bd8d
4 changed files with 231 additions and 9 deletions
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# Prepare a Claude Code on the web container for work on this repository.
#
# Three things are slow or wrong by default in a fresh cloud session:
#
# 1. `bd` is not installed, so the `bd prime` hook and every `bd` command
# in AGENTS.md silently do nothing.
# 2. `.beads/issues.jsonl` is a stale export. An agent that reads it picks
# work that is already done — this has happened. The authoritative
# tracker is the Dolt database on the git remote.
# 3. The Rust workspace is uncompiled, so the first `cargo test` pays a
# cold build before it reports anything.
#
# Idempotent: every step checks before acting. Nothing here writes to a
# tracked file, stages anything, or creates a commit.
set -uo pipefail
ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
cd "$ROOT" || exit 0
# Local checkouts have their own setup; this is for the web containers.
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi
log() { printf '[session-start] %s\n' "$*"; }
# ── 1. bd on PATH ──────────────────────────────────────────────────────────
# The embedded Dolt engine needs CGO, so this builds from source (~3 min).
# The container image is cached after the hook, so it is paid once.
export PATH="$PATH:/root/go/bin:$HOME/go/bin"
if ! command -v bd >/dev/null 2>&1; then
log "installing bd (beads)…"
if curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh \
| bash >/tmp/bd-install.log 2>&1; then
log "bd installed"
else
log "WARNING: bd install failed; see /tmp/bd-install.log. Issue tracking unavailable."
fi
fi
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
echo 'export PATH="$PATH:/root/go/bin:$HOME/go/bin"' >> "$CLAUDE_ENV_FILE"
fi
command -v bd >/dev/null 2>&1 && bd metrics off >/dev/null 2>&1
# ── 2. Hydrate the issue tracker from the Dolt remote ──────────────────────
# `bd init` in-place rewrites .beads/config.yaml and commits, so the sync
# happens in a throwaway directory and only the database is copied back.
# The directory must be named after the database (see .beads/metadata.json).
if command -v bd >/dev/null 2>&1 && [ ! -d .beads/embeddeddolt ]; then
DB_NAME="$(jq -r '.dolt_database // "improvise"' .beads/metadata.json 2>/dev/null || echo improvise)"
ORIGIN="$(git remote get-url origin 2>/dev/null || true)"
case "$ORIGIN" in
git@github.com:*) ORIGIN="https://github.com/${ORIGIN#git@github.com:}" ;;
esac
ORIGIN="${ORIGIN%.git}"
if [ -n "$ORIGIN" ]; then
log "syncing issue tracker from $ORIGIN …"
TMP="$(mktemp -d)"
mkdir -p "$TMP/$DB_NAME"
(
cd "$TMP/$DB_NAME" && git init -q . &&
BD_NON_INTERACTIVE=1 bd init --non-interactive --skip-agents --skip-hooks -q \
--remote "git+$ORIGIN"
) >/tmp/bd-sync.log 2>&1
if [ -d "$TMP/$DB_NAME/.beads/embeddeddolt" ]; then
cp -rf "$TMP/$DB_NAME/.beads/embeddeddolt" .beads/embeddeddolt
log "issue tracker synced — 'bd ready' now reflects the remote, not issues.jsonl"
else
log "WARNING: tracker sync failed; see /tmp/bd-sync.log."
log " .beads/issues.jsonl is a STALE export — verify any bead against the code."
fi
rm -rf "$TMP"
fi
fi
# ── 3. Warm the build so the first cargo test is not a cold compile ────────
if command -v cargo >/dev/null 2>&1; then
log "warming cargo build…"
cargo build --workspace --tests >/tmp/cargo-warm.log 2>&1 \
&& log "workspace built" \
|| log "WARNING: cargo build failed; see /tmp/cargo-warm.log"
fi
# ── 4. Environment facts worth knowing up front ────────────────────────────
cat <<'NOTES'
[session-start] Environment notes (see context/cloud-agents.md):
• Use the preinstalled cargo toolchain. `nix develop` cannot resolve this
flake here — the proxy blocks GitHub tarball fetches (403).
• `bd dolt push` is blocked outbound (403). Bead status changes stay local.
• Never run a bare `bd init` in this repo: it rewrites AGENTS.md, CLAUDE.md
and .claude/settings.json, and commits. Use --skip-agents --skip-hooks.
• `bd dolt push <remote>` ignores its argument and pushes to origin.
NOTES
exit 0
+38 -9
View File
@@ -1,26 +1,55 @@
{
"permissions": {
"allow": [
"Bash(cargo build:*)",
"Bash(cargo test:*)",
"Bash(cargo clippy:*)",
"Bash(cargo fmt:*)",
"Bash(cargo run -- cmd:*)",
"Bash(cargo run -- script:*)",
"Bash(bd ready:*)",
"Bash(bd list:*)",
"Bash(bd show:*)",
"Bash(bd prime:*)",
"Bash(bd stats:*)",
"Bash(bd update:*)",
"Bash(bd close:*)",
"Bash(bd create:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git branch:*)",
"Bash(git fetch:*)",
"Bash(git ls-remote:*)"
]
},
"hooks": {
"PreCompact": [
{
"matcher": "",
"hooks": [
{
"command": "bd prime",
"type": "command"
"type": "command",
"command": "bd prime"
}
],
"matcher": ""
]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"command": "bd prime",
"type": "command"
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
},
{
"type": "command",
"command": "bd prime"
}
],
"matcher": ""
]
}
]
}
}
}
+95
View File
@@ -0,0 +1,95 @@
# Cloud Agents (LLM Reference)
Environment notes for agents running in Claude Code on the web. Local
checkouts are unaffected — the session hook exits early unless
`CLAUDE_CODE_REMOTE=true`.
`.claude/hooks/session-start.sh` handles the setup automatically. This doc
is the "why", and the record of what bites when it doesn't run.
---
## The issue tracker is not `issues.jsonl`
**`.beads/issues.jsonl` is a stale export.** It is a viewer/interchange
artifact, not the source of truth — `.beads/README.md` says as much
("Cross-machine sync and backups use Dolt remotes/backups, not JSONL
import/export"). The real tracker is the Dolt database published to
`refs/dolt/data` on the git remote.
The gap is not cosmetic. A session that hydrated from the JSONL saw 121
issues against the remote's 168, with every P1 bug showing OPEN though all
four were fixed and closed. Two beads were implemented from scratch that
the remote had recorded as closed weeks earlier, and which turned out to
be already implemented on an unmerged branch.
The hook syncs the database from the remote at session start. If it did
not run, or reported a failure:
```bash
bd list --all | wc -l # 121 means you are on the stale export
```
**Verify any bead against the code before starting it.** `git log --oneline
--all | grep -oE 'improvise-[a-z0-9]{3}'` names the ones already
referenced by a commit, and `git log origin/main` is worth a look for work
that landed without a bead reference.
---
## `bd init` is destructive here
A bare `bd init` in this repo rewrites `AGENTS.md`, `CLAUDE.md` and
`.claude/settings.json`, installs Cursor/Codex integration files, and
**commits all of it**. It also rewrites `sync.remote` in
`.beads/config.yaml`.
Always pass the opt-outs:
```bash
bd init --non-interactive --skip-agents --skip-hooks -q --remote git+<origin>
```
Even then it commits a small `.gitignore` change, so the hook runs it in a
throwaway directory and copies only `.beads/embeddeddolt` back. Do the
same if you need to re-sync by hand; never run it in the working tree.
---
## What does not work in this environment
| Thing | Status | Use instead |
|---|---|---|
| `nix develop` | Fails: flake inputs are GitHub **tarballs**, and the proxy answers 403 | The preinstalled `cargo`/`rustc` — same stable toolchain the flake pins |
| `bd dolt push` | Fails: 403 outbound | Nothing; bead status changes stay local. Say so rather than implying the tracker was updated |
| `git+ssh://` Dolt remote in `config.yaml` | No ssh client in the container | `git+https://<origin>`, which the hook passes explicitly |
| `git push`, GitHub API | **Work normally** | — |
Nix itself installs and runs fine (no init system, so the daemon needs
starting by hand); only flake-input resolution is blocked. Reads of public
GitHub repos over the git protocol work — it is specifically the
`/archive/*.tar.gz` and `codeload` endpoints that are refused.
---
## Sharp edges in `bd`
- **`bd dolt push <remote>` ignores its argument** and pushes to `origin`.
Pointing it at a throwaway remote does not keep the real one out of it;
only the 403 does. Treat the named form as unsafe.
- `bd dolt pull` into a divergent local database fails with `local changes
would be stomped by merge`. Re-sync from the remote instead of merging.
- `bd` warns if `.beads` is not mode 0700.
---
## Working conventions
- One bead per pull request. Stacking a second bead onto an open PR has
been rejected in review.
- A merged PR cannot carry follow-up work: restart from the latest default
branch and open a new PR.
- Prefer merges to rebasing (`AGENTS.md`), including when bringing the
default branch into a working branch.
- Keep `context/repo-map.md` and `context/design-principles.md` current —
they are the two docs agents are expected to maintain by hand.
+1
View File
@@ -272,6 +272,7 @@ examples/pretty-print.rs Parse stdin, print formatted .improv
### Context docs
```
context/cloud-agents.md Cloud-session environment: tracker sync, bd pitfalls, what is blocked
context/design-principles.md Architectural principles & testing doctrine
context/plan.md Show HN launch plan
context/repo-map.md This file