Agent Skills: Session Selfcheck

>

UncategorizedID: agentydragon/ducktape/web_selfcheck

Install this agent skill to your local

pnpm dlx add-skill https://github.com/agentydragon/ducktape/tree/HEAD/devinfra/claude/skills/web_selfcheck

Skill Files

Browse the full folder contents for web_selfcheck.

Download Skill

Loading file tree…

devinfra/claude/skills/web_selfcheck/SKILL.md

Skill Metadata

Name
web_selfcheck
Description
>

Session Selfcheck

This skill is the runnable acceptance test for the Rust hook daemon specification at <../../claude_hook/SPEC.md>.

How to use this skill

  1. Read SPEC.md first. It enumerates every behavior a healthy session must satisfy, split into ### Common, ### CLI only, and ### Web only under the ## Observable Acceptance Criteria heading. The SPEC is the source of truth. If the SPEC and this skill disagree, the SPEC wins — update the skill.
  2. Detect the profile. $DUCKTAPE_CLAUDE_HOOKS_PROFILE (or the file path that the daemon was launched with) tells you whether to run the CLI or Web criteria. Always run the Common criteria.
  3. For each SPEC criterion, run the matching check from the "SPEC acceptance checks" section below.
  4. Then run the out-of-SPEC diagnostics section, which catches real-world failure modes the SPEC does not (yet) codify.
  5. Produce the report using the format at the end.

Run all Bash commands with dangerouslyDisableSandbox: true (needs network and filesystem access outside the sandbox). Run independent checks in parallel where possible.

CRITICAL: observe only — do NOT fix without explicit user approval

This is a diagnostic skill. Treat a broken session like a crime scene: observe, document, and report — do not touch.

Do NOT run any remediation commands (e.g. web_setup.sh, re-triggering SessionStart, sourcing env files, installing packages, re-running git remote add) unless the user explicitly says to proceed. If a check fails, the fix is "the daemon is broken, tell the user" — not "let me work around it."

Exception — debugging workarounds: when the session hooks are demonstrably broken and you are actively debugging or documenting, the following lightweight workarounds are acceptable without explicit approval:

  • Committing with hooks bypassed: git commit --no-verify to record diagnostic work while hooks are broken
  • Unsetting BUILDBUDDY_API_KEY to force local bazel when bbr is broken
  • Creating a bazel wrapper in the session bin that injects --bazelrc when the session bazelrc exists but the shim is missing

Log file inventory

When diagnosing a broken session, these are the log files worth reading, in order of "most likely to contain the smoking gun":

| Path | What's in it | | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | /tmp/claude-hd/<sid>/daemon.err.log | Unhandled hook daemon errors — check first. | | /tmp/claude-hd/<sid>/daemon.log | Daemon stdout: startup and per-hook diagnostics. | | /tmp/claude-hd/<sid>/startup_failure.json | Written when the claude-hook client could not reach/start the daemon. Distinguishes "dispatcher timed out waiting for socket" from "daemon crashed after accepting connection". | | ~/.claude/session-env/<sid>/sessionstart-hook-0.sh | The env file the daemon wrote. Presence means SessionStart got far enough to write the agent shell environment. | | ~/.claude/session-env/<sid>/supervisor/supervisord.log | Supervisor daemon log (only populated when the container-runtime profile is in use). | | /tmp/web-setup.log | web_setup.sh output from the most recent run. First line has web_setup.sh commit: <sha> — use it to detect stale setup scripts. |

<sid> can be resolved with:

LIVE=$(ps aux | grep 'claude-hook daemon' | grep -v grep | grep -oP '(?<=--sock /tmp/claude-hd/)[^/]+')
echo "$LIVE"

or from $CLAUDE_ENV_FILE (the basename of its parent directory).

Agent discipline: when a check below fails, dump the relevant log section verbatim into the report's "Issues & remediation" block. Do not paraphrase tracebacks — the exact text is what the user needs to correlate with git log.

SPEC acceptance checks

Each check below corresponds one-to-one with a numbered criterion in SPEC.md. Cross-reference the SPEC for the authoritative statement of what the check is verifying.

Common

C1 — BUILDBUDDY_API_KEY is present; validity is proven by C3.

[ -n "${BUILDBUDDY_API_KEY:-}" ] && echo "present (len=${#BUILDBUDDY_API_KEY})" \
  || echo "FAIL: BUILDBUDDY_API_KEY unset"

Presence only — there is no working lightweight HTTP probe. The remote.buildbuddy.io gateway returns 415 for the old GetUser curl with or without the key (it rejects the content type before auth), so that probe cannot distinguish a valid key. The authoritative validity test is the live RBE build in C3 / W4: a 401/403 or a BES auth rejection there is the real signal of a bad key.

C2 — GITHUB_TOKEN is present and valid.

curl -s -H "Authorization: Bearer ${GITHUB_TOKEN}" https://api.github.com/user \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('login:', d.get('login'), 'message:', d.get('message',''))"

Pass on web: login: agentydragon-agent. Pass on CLI: the user's own GitHub login. Bad credentials = expired/revoked.

C3 — bbr build <trivial> succeeds without TLS or proxy errors.

cd /home/user/ducktape
bbr build //devinfra:gazelle --nobuild 2>&1 | tail -5

Pass: exit 0 with no Unable to resolve host, certificate, 127.0.0.1:*, or proxy errors.

C4 — bazelisk shim is active and invocations are session-tagged.

LIVE=${CLAUDE_ENV_FILE:+$(basename "$(dirname "$CLAUDE_ENV_FILE")")}
LIVE=${LIVE:-$(ps aux | grep '[c]laude-hook daemon' | grep -oP '(?<=--sock /tmp/claude-hd/)[^/]+' | head -1)}
SESSION_DIR="$HOME/.claude/session-env/$LIVE"
ls -l "$(command -v bazelisk)"  # must point into $SESSION_DIR/bin
grep -E 'build_metadata|TAGS' "$SESSION_DIR/bbr.bazelrc" 2>/dev/null

Pass: bazelisk resolves inside the session dir, and bbr.bazelrc contains session:<id> metadata.

C5 — bbr imports the generated session metadata.

Covered by the bbr.bazelrc metadata check above and by checking the BuildBuddy invocation tags after a real bbr invocation.

C6 — throwaway-commit pre-commit end-to-end.

cd /home/user/ducktape
START_BRANCH=$(git rev-parse --abbrev-ref HEAD)
TEST_BRANCH="selfcheck/$(date +%s)"
TEST_FILE=/home/user/ducktape/selfcheck-tmp.txt   # kebab-case: avoids the filename-convention hook
trap 'git checkout -q "$START_BRANCH"; git branch -D "$TEST_BRANCH" 2>/dev/null; rm -f "$TEST_FILE"' EXIT
git checkout -q -b "$TEST_BRANCH"
printf 'selfcheck %s\n' "$(date -Iseconds)" > "$TEST_FILE"
git add "$TEST_FILE"
git commit -m "test: selfcheck — delete me" 2>&1 | tail -40
echo "exit: ${PIPESTATUS[0]}"

Pass: exit 0 with every hook Passed/Skipped. No commit-msg trailer is required — a bare commit message is expected to go through.

C7 — hook daemon logs present, no unhandled exceptions.

LIVE=${CLAUDE_ENV_FILE:+$(basename "$(dirname "$CLAUDE_ENV_FILE")")}
LIVE=${LIVE:-$(ps aux | grep '[c]laude-hook daemon' | grep -oP '(?<=--sock /tmp/claude-hd/)[^/]+' | head -1)}
LOG="/tmp/claude-hd/$LIVE/daemon.log"
[ -f "$LOG" ] && grep -cE 'ERROR|Traceback|Exception' "$LOG" || echo MISSING

Pass: log exists, zero matches (or only expected warnings — use judgement).

C8 — OTLP tracing reaches the collector.

curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer ${DUCKTAPE_OTEL_BEARER_TOKEN}" \
  -H "Content-Type: application/json" -d '{}' \
  https://alloy-otlp.allegedly.works/v1/traces

Pass: 200 or 400 (bad proto = auth passed). 401 = token rotated or missing.

C9 — bbr preserves the analysis cache on a second identical run.

Low-precision, high-recall sensor with a high false-positive rate (runner rotation, BB server restart, cache eviction can all cause transient cold hits). Report the finding but don't act on a single failure. Stop early rather than spending many minutes retrying.

Method — cache poisoning: append a comment to MODULE.bazel so the first build is guaranteed cold, then time an immediately-following identical build. The SPEC permits occasional cold-hits; only flag if warm ≈ cold across two repeated runs.

cd /home/user/ducktape
echo "# selfcheck-poison-$(date +%s)" >> MODULE.bazel
T1_START=$(date +%s%N)
bbr build //... --nobuild 2>&1 | tail -3
T1_SEC=$(( ($(date +%s%N) - T1_START) / 1000000000 ))
T2_START=$(date +%s%N)
bbr build //... --nobuild 2>&1 | tail -3
T2_SEC=$(( ($(date +%s%N) - T2_START) / 1000000000 ))
git checkout -- MODULE.bazel
echo "cold=${T1_SEC}s warm=${T2_SEC}s"

Interpret: warm < cold/3 = recycling works. warm ≈ cold = likely not recycling (but re-run before diagnosing — high FP rate). cold < 5s = build graph too small to measure. If consistently warm≈cold across two runs, inspect bbapi invocation <id> for runner IDs.

CLI only

CLI1 — git commit --amend is blocked by the git shim.

(cd /tmp && git init -q selfcheck && cd selfcheck && \
  git commit --allow-empty -m init -q 2>/dev/null && \
  git commit --amend --no-edit 2>&1 | grep -c '\[git-shim\] BLOCKED')
rm -rf /tmp/selfcheck

Pass: 1.

CLI2 — git add -A / git add . is blocked.

(cd /tmp && mkdir -p selfcheck2 && cd selfcheck2 && \
  git init -q && git add -A 2>&1 | grep -c '\[git-shim\] BLOCKED')
rm -rf /tmp/selfcheck2

Pass: 1.

CLI3 — git stash is blocked (but list / show allowed).

git stash 2>&1 | grep -c '\[git-shim\] BLOCKED'
git stash list 2>&1 | grep -c '\[git-shim\] BLOCKED'  # must be 0

CLI4 — direnv bridge propagates .envrc exports into Bash tool calls.

# Expect a representative env var (e.g. one set only by .envrc) to appear
# after cd into a subproject that has one.
cd /home/user/ducktape && env | grep -c '^DUCKTAPE_CLAUDE_HOOKS_PROFILE='

Pass: 1 (or whatever var your .envrc exports).

Web only

W1 — kubectl works as claude-code-web; MCP returns the same pods.

kubectl -n claude-sandbox get pods 2>&1 | tail -5

Then invoke the mcp__kubectl-local__pods_list_in_namespace tool with namespace=claude-sandbox and compare. Pass: both succeed and agree.

W2 — $GITHUB_TOKEN identifies as agentydragon-agent.

Covered by C2 on web — no separate check.

W3 — bbr build <any target> works out of the box, no manual remote setup, no remote picker.

cd /home/user/ducktape
# Run interactively — the test fails if bb prints a remote picker prompt
# or errors on missing git config.
timeout 60 bbr build //devinfra:gazelle --nobuild 2>&1 | tail -10

Pass: exit 0, no "which remote" prompt, no Unable to resolve host, no 127.0.0.1:* in the runner's origin URL.

W5 — Docker (non-goal: the Rust daemon does not set one up).

docker info >/dev/null 2>&1 && echo "present" || echo "absent (expected)"

The web profile's setup_docker key is ignored by the Rust daemon — only the retired Python daemon started Docker (see devinfra/claude/TODO.md). So an absent local Docker daemon is expected, not a failure: Docker-dependent tests run on BuildBuddy RBE workers, not locally. Report present/absent informationally; only flag if a workflow genuinely needs local Docker.

Out-of-SPEC diagnostics

These are not in SPEC.md but catch real-world failure modes. Include them in the report under a separate "Diagnostics" heading.

Before running D1/D2, skim <../../docs/web-setup-debug.md> — it documents the historical failure modes (SHA-pinned setup URLs, the Firecracker "pin drift on persistent rootfs" class, the Nix 2.34.3 SIGABRT masking issue) and is the authoritative reference for how web_setup.sh is supposed to behave. In particular, the "Pin drift on persistent rootfs" section explains why a container running for more than a day or two can silently have a stale claude-hooks wheel even though web_setup.sh re-runs every session, and gives the readlink /nix/var/nix/profiles/default/bin/claude-hook diagnostic below.

D1 — web_setup.sh freshness (web only)

Anthropic reuses Firecracker microVMs; /tmp/web-setup.log may be from a prior session running an older web_setup.sh. A stale setup means Nix devtools and skills may not match the current code.

ls -la /tmp/web-setup.log 2>/dev/null || echo "MISSING"
tail -3 /tmp/web-setup.log 2>/dev/null                    # last line should be "Setup complete."
SETUP_COMMIT=$(grep 'web_setup.sh commit:' /tmp/web-setup.log 2>/dev/null | tail -1 | grep -oE '[0-9a-f]{40}')
HEAD_COMMIT=$(git -C /home/user/ducktape rev-parse HEAD)
[ "$SETUP_COMMIT" = "$HEAD_COMMIT" ] && echo "OK" || echo "STALE: setup=$SETUP_COMMIT head=$HEAD_COMMIT"

D2 — claude-hooks daemon pin staleness

A stale installed daemon is often the root cause of session hook failures. There are two independent kinds of staleness to check:

(a) Pin in nix/artifact-pins.json is behind HEAD — sync-pins.yml didn't run recently, or release.yml is failing. The repo itself is out of date. On a shallow clone (Claude Code web clones ~50 commits — check .git/shallow), the pinned commit is usually absent locally, so the git log / git merge-base ancestry checks below are unreliable. Compare the pin SHA against origin/devel via the GitHub MCP instead, or just confirm daemon.err.log shows no schema-drift crashes (the practical signal).

(b) Installed wheel is behind the pin — on Firecracker web sessions with a persistent rootfs, nix profile install is a no-op when devtools is already installed, so the on-disk wheel can freeze at first-boot even though nix/artifact-pins.json has moved forward. This is the class of failure described in <../../docs/web-setup-debug.md> "Pin drift on persistent rootfs". Typical symptom: SessionStart crashes with 'Undefined' object has no attribute '<field>' in daemon.err.log, or silently missing env vars because a new profile.yaml field was dropped by Pydantic.

# (a) Pin in artifact-pins vs HEAD
python3 -c "
import json, re
pins = json.load(open('/home/user/ducktape/nix/artifact-pins.json'))['pins']
url = pins.get('claude-hooks', {}).get('url', '')
m = re.search(r'claude-hooks-([0-9a-f]+)', url)
print('pinned:', m.group(1) if m else 'unknown')
"
git -C /home/user/ducktape log --oneline -5 -- devinfra/claude/ nix/artifact-pins.json

# (b) Installed wheel vs pin
claude-hook --version  # Rust binary: prints the crate version (e.g. 0.0.0), not a git stamp
# Check daemon.err.log for template/schema crashes that indicate drift
tail -50 /tmp/claude-hd/*/daemon.err.log 2>/dev/null

If (a) the pin is behind HEAD, diff the installed Nix store package against the repo source for breaking changes (renamed classes, changed config paths, removed hooks). Check GitHub CI on agentydragon/ducktape: recent release.yml and sync-pins.yml runs on devel.

If (b) the installed wheel is behind the pin, the remediation is to re-run bash devinfra/claude/web_setup.sh — but do not do this unprompted per the "observe only" rule above. Report the drift in the Issues section with exact commit SHAs and let the user decide.

D3 — git remote origin URL reachability (web only)

Known failure mode: bb remote reads git remote -v locally and sends the URL to the cloud runner. If origin is 127.0.0.1:* (Claude Code web session proxy), the runner can't reach it and bbr fails on hook invocations.

ORIGIN=$(git -C /home/user/ducktape remote get-url origin)
echo "$ORIGIN" | grep -qE '127\.0\.0\.1|localhost' && echo "WARN: local proxy origin" || echo "OK"
git -C /home/user/ducktape config buildbuddy.remote-bazel-default-remote 2>/dev/null \
  && echo "(buildbuddy remote override is set)" \
  || echo "(no remote override)"

Report Format

# Session Selfcheck — <timestamp>

Profile: <CLI/Web>    Summary: <healthy / degraded / broken>

## SPEC acceptance criteria

| ID  | Check                          | Status   | Detail                |
| --- | ------------------------------ | -------- | --------------------- |
| C1  | BUILDBUDDY_API_KEY valid       | OK/FAIL       | HTTP <code>           |
| C2  | GITHUB_TOKEN valid             | OK/FAIL       | login=...             |
| C3  | bbr build trivial              | OK/FAIL       | ...                   |
| C4  | bazelisk shim + session tag    | OK/FAIL       | ...                   |
| C5  | bbr imports session metadata   | OK/FAIL       | session:<id> tag      |
| C6  | throwaway commit end-to-end    | OK/FAIL       | ...                   |
| C7  | daemon log clean               | OK/FAIL       | N errors              |
| C8  | OTLP tracing                   | OK/FAIL       | HTTP <code>           |
| C9  | bbr analysis cache warm        | OK/WARN/AMBIG | cold=Xs warm=Ys       |
| CLI1–4 / W1–4                   | ...           | ...                   |

## Out-of-SPEC diagnostics

| ID | Check                          | Status        | Detail                 |
| -- | ------------------------------ | ------------- | ---------------------- |
| D1 | web_setup.sh freshness         | OK/STALE/MISS | setup=<sha> head=<sha> |
| D2 | claude-hooks pin staleness     | OK/BEHIND     | pin=<sha>, CI status   |
| D3 | origin URL reachable for bbr   | OK/WARN       | origin=...             |

## Issues & remediation

### <issue title>
**Spec criterion violated**: <ID>
**Impact**: <what's broken for the agent>
**Root cause**: <why>
**Fix** (for the user to run, not the skill): <exact commands>

Prioritize: SPEC violations first (the daemon is broken), then out-of-SPEC diagnostics.