# The day the reviewer agent inlined the Gitea token in a Bash command — and v0.23.4 redacted it anyway

> Source: <https://loomcycle.dev/blog/the-day-the-reviewer-agent-inlined-the-token.html>
> Published: 2026-06-08 16:00:00+00:00

Day four of the operator-via-MCP experiment series. Real Gitea on the tailnet, real third-party MCP (gitea-mcp, 53 tools), real Telegram bot. Closed-loop dev workflow runs end-to-end un-bridged: the coder agent opens a real PR, a Gitea pull_request webhook auto-spawns the reviewer with the full signed payload, the reviewer reviews + merges, a second webhook spawns the advisor, Telegram lights up. By v0.23.5 every entity in the loop can be runtime-created (agents, MCP server, channels, webhooks) — no static yaml. The headline moment came mid-merge: the reviewer agent inlined the resolved GITEA_TOKEN literally in a Bash command (GITEA_TOKEN="..." git -c http.extraheader=...) and then ran `env` for good measure. The token is now in two distinct tool-call records (input + result text). Pre-v0.23.4 this would have persisted to the SQLite transcript store as plaintext, retrievable by anyone who grabbed the DB file. v0.23.4 (#407) ships value-based secret redaction at rest: the runtime resolves the values of every LOOMCYCLE_* env var at boot, scans persistence-bound payloads (tool-call input + result text) for substring matches, and replaces every occurrence with [redacted:]. The match is value-based, so an agent that assigns the token to a differently-named shell var (GITEA_TOKEN=... instead of LOOMCYCLE_GITEA_TOKEN) is still caught. The replacement preserves the env-var NAME for debuggability — the operator can see WHICH secret got used where without seeing the value. Whole-DB scan after reproducing the leak under v0.23.4 with the secret pulled from .env.local + piped through strings | grep -f: 0 literal token hits, 0 webhook-secret hits, 32 env-var-name references. Plus three supporting fixes that completed the fully-dynamic version of the workflow: F30 (#403) webhook → dynamic agent — WebhookDef now stamps TenantID from the run identity (mirroring AgentDef), so a runtime webhook resolves a runtime agent under the same tenant (v0.23.2 failed with rejected_spawn_setup: unknown agent). F31 (#405) gated dynamic stdio MCP — LOOMCYCLE_MCP_ALLOW_DYNAMIC_STDIO=1 opt-in flag permits POST /v1/_mcpserverdef with transport:stdio + command/args/env; off by default. The token-safe pattern uses a tiny shell wrapper (gitea-mcp-stdio.sh: env GITEA_ACCESS_TOKEN="$LOOMCYCLE_GITEA_TOKEN" exec gitea-mcp -t stdio) so the secret flows env → env → env, never entering a tool argument. F33 (#409) dynamic MCP tools advertised at run start — a single-purpose MCP-only notifier agent (like the advisor with allowed_tools ["mcp__telegram-dyn__*"] only) can now enter tool-calling mode without needing Context as a discovery crutch. Two webhook discoverability footguns from the v0.23.0 incarnation of exp4 also fixed in v0.23.1/v0.23.2: F23 (#385) HMAC secret allowlist three-rule model so zero allowlist config needed for the common case, F28 (#387) default the webhook payload to the raw signed body when no `goal` mapping is configured. The unifying lesson: store names, never values — and enforce it value-based, so the agent's typo doesn't undo your discipline. The residual caveat worth naming: the Bash process the agent spawns still has the real environment, so at runtime the secret is in process memory; the redaction is "value-at-rest" (the persistence boundary), not "value-out-of-process" (a separate, larger sandbox-design question).
