gitea-mcp authenticates by PAT pass-through, so an agent holds the
caller's full PAT scope, and repo_delete's confirm field is filled in by
the model. The infra-mcp README said client-side deny lists covered
destructive verbs; on 2026-10-01 none existed in any settings file.
PreToolUse hook on mcp__.*: repo_delete, file_delete, branch_delete,
pr_merge, tbd_ship, repo_update and repo_mirror_push get "ask". Matched
on the verb, not the server name, so the connector namespace or a
renamed registration cannot bypass it. Never emits allow; malformed
input fails closed.
Scope limit: MCP calls only. The Gitea REST API via Bash with the PAT
in env is not covered (recorded in infra ADR-0027).
Refs mathias/infra#508
Bump-Type: minor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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