2 Commits
Author SHA1 Message Date
mathiasandClaude Opus 5 88c54399fe feat(doctor): fail loud on skills drift, wired to SessionStart
Every failure this fleet actually hit was silent. 243 dangling symlinks were
invisible to git because **/.claude/skills/ is in the global gitignore. A
checkout 20 skills behind still resolved, so nothing complained. A loose
secret-hygiene.md loaded in no harness and errored in none. Two machines had
the library missing entirely. Exit status is the signal that was absent.

bin/doctor reports, with a non-zero exit:
  - dangling links, anywhere under the harness dirs
  - links that RESOLVE but land outside the active checkout — the dangerous
    case, because it works while serving stale content
  - skill-count mismatch between a harness and the checkout
  - stray entries where <name>/SKILL.md was expected
  - leftover per-skill Claude symlinks (plugin skills do not override
    same-named user skills, so leftovers mean everything loads twice)
  - divergence between the plugin copy and the copy the other harnesses link to

Wired as a SessionStart hook with --quiet, so a healthy host prints nothing and
a drifting one says so at the top of the next session. That beats a timer:
no launchd/systemd wiring, and it reaches every host that installs the plugin.
27ms on koala, no network, no git fetch.

Two design points found by the tests, both worth keeping:
  - The canonical checkout is whatever ~/.agents/skills resolves to, NOT the
    copy doctor runs from. As a plugin hook $0 lives in the plugin cache, which
    is legitimately a second copy; judging harness links against it flagged
    every one of them as "outside".
  - "cd X/.." resolves ".." lexically against the link path and lands back in
    ~/.agents. Resolve the link first, then take the parent.

tests/doctor_test.sh drives seven fixtures through $HOME and $DOCTOR_CHECKOUT,
so no test touches the real host. Each asserts both the non-zero exit and that
the report names the fault — an exit-code-only assertion would pass while the
message said nothing useful.

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-23 14:34:32 +02:00
mathiasandClaude Opus 5 747c6c7f4c feat(plugin): ship the secret-guard and pre-task-ritual hooks + MCP servers
The plugin now carries the two hooks Mathias owns and the brain/gitea/infra
MCP server definitions, not just skills.

Why this matters more than tidiness: both hooks existed ONLY in koala's
~/.claude/, which is host-local and unsynced. flamingo and iguana have been
running with no secret-guard at all — that is the control which blocks the
decrypt-and-print shape behind the 2026-08-05 ntfy-token leak, the op-item-get
shape behind 2026-07-30, and the base64-bypass shape behind 2026-06-11. Its
absence on two of three machines is a security gap, not a config gap. Shipping
it in the plugin means installing the plugin installs the guard.

.mcp.json references tokens as ${BRAIN_MCP_TOKEN} / ${GITEA_MCP_TOKEN} only —
env indirection, never literal values, so the manifest is safe to commit.

tests/secret_guard_test.sh pins 14 cases. It asserts on
hookSpecificOutput.permissionDecision, NOT exit status: the guard always exits
0 by contract, so an exit-status assertion would pass vacuously while the guard
silently allowed everything. Each deny case names the incident that produced it.

gsd-* hooks are deliberately NOT migrated — GSD owns and updates those in place.

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-23 14:31:04 +02:00