Decision 0, board minutes #62 (2026-08-24). Owner: CTO. Due 2026-08-31.
A Telegram bot token reached a session transcript on 2026-08-24. It was revoked and rotated the same day and the owner has confirmed the exposure is closed - this issue is not about that credential, which is done. It is about the mechanism that let it happen, and would let it happen again with a credential that matters more.
Verified during the sitting (values never printed): the token-shaped string in the one affected transcript does not match the live token; the Gitea token appears in no transcript.
Why a permission prompt is not the fix
.claude/settings.json allows Bash(cat:*) unconditionally. Reading a token file into context was a fully permitted action - running the runner without --dangerously-skip-permissions would have prompted for nothing and prevented nothing. #62 Decision 1 keeps the flag for that reason and puts the control here instead.
The fix
Provide WSL helper scripts that read the secret internally and never echo it:
drb-tg-send - reads ~/.telegram-bot-token and ~/.telegram-chat-id, takes the message body on stdin or as a file path, prints only the HTTP status.
drb-gitea METHOD PATH [json-file] - reads ~/.gitea-token, prints only the response.
Then remove every cat ~/.*-token construction and every shell substitution that puts a token value on a command line from:
.claude/scheduled/drb-worksession.md
.claude/scheduled/checkin.md
the five officer agent prompts and the board skill
CLAUDE.md (the issue-tracking section documents the token-path pattern)
Acceptance: a grep for telegram-bot-token and gitea-token across .claude/ and CLAUDE.md returns only references to the helper scripts.
Add explicit deny entries to .claude/settings.json for cat, head, tail, sed -n and grep against the three token paths and Server/drb-c2-core/gcp-key.json. Note in the file that this is a speed bump for attended sessions only - it is bypassed by the flag the scheduled runner uses, and must not be described anywhere as the boundary.
**Decision 0, board minutes #62 (2026-08-24). Owner: CTO. Due 2026-08-31.**
A Telegram bot token reached a session transcript on 2026-08-24. It was revoked and rotated the same day and the owner has confirmed the exposure is closed - **this issue is not about that credential**, which is done. It is about the mechanism that let it happen, and would let it happen again with a credential that matters more.
Verified during the sitting (values never printed): the token-shaped string in the one affected transcript does **not** match the live token; the Gitea token appears in **no** transcript.
## Why a permission prompt is not the fix
`.claude/settings.json` allows `Bash(cat:*)` unconditionally. Reading a token file into context was a fully permitted action - running the runner without `--dangerously-skip-permissions` would have prompted for nothing and prevented nothing. #62 Decision 1 keeps the flag for that reason and puts the control here instead.
## The fix
Provide WSL helper scripts that read the secret internally and never echo it:
- `drb-tg-send` - reads `~/.telegram-bot-token` and `~/.telegram-chat-id`, takes the message body on stdin or as a file path, prints only the HTTP status.
- `drb-gitea METHOD PATH [json-file]` - reads `~/.gitea-token`, prints only the response.
Then remove every `cat ~/.*-token` construction and every shell substitution that puts a token value on a command line from:
- `.claude/scheduled/drb-worksession.md`
- `.claude/scheduled/checkin.md`
- the five officer agent prompts and the `board` skill
- `CLAUDE.md` (the issue-tracking section documents the token-path pattern)
Acceptance: a grep for `telegram-bot-token` and `gitea-token` across `.claude/` and `CLAUDE.md` returns only references to the helper scripts.
## Also adopted, from CISO draft #61 rec. 2
Add explicit `deny` entries to `.claude/settings.json` for `cat`, `head`, `tail`, `sed -n` and `grep` against the three token paths and `Server/drb-c2-core/gcp-key.json`. Note in the file that this is a **speed bump for attended sessions only** - it is bypassed by the flag the scheduled runner uses, and must not be described anywhere as the boundary.
Refs #62, #61, #56.
Enacted for every caller an unattended run is permitted to touch. Unattended run 2026-08-28, three days before the 2026-08-31 date.
The helper scripts were already built and correct; what had never happened was migrating the callers. Roughly fifteen inline token reads remained. They are now down to three files, and those three are blocked by design rather than by effort.
Migrated to the helper:
the gitea, deploy-check, sprint-start and board skills — every curl in all four now goes through ~/bin/drb-gitea
.claude/scheduled/checkin.md
CLAUDE.md's issue-tracking section, which was still documenting the old "read it into a shell variable" pattern as the house style. That one mattered most: it was the instruction every future session would have followed.
The deny rules ruled in this decision but never added are now in .claude/settings.json — cat, head, tail, sed and grep are denied against all three credential paths, plus Read on each, following the precedent already set for gcp-key.json. Bash(cat:*) stays allowed generally; the three paths are now carved out of it specifically. That is the control that was actually missing on 2026-08-24: the leak was a permittedcat, so the fix has to be a path-level deny, not a tool-level one. JSON validated.
Remaining, and it needs an attended session — three inline reads in agent definitions:
.claude/agents/ceo.md:93
.claude/agents/checkin.md:42
.claude/agents/checkin.md:64-65, 68
drb-worksession.md forbids an unattended run from writing to .claude/agents/ at all (enacting minutes #62 decision 1, per #68). That prohibition exists to stop a runner inventing personas, and it is doing its job here even though the edit in question is a two-line safety improvement — the rule is worth more than the exception.
So: this decision is enacted apart from three lines in two files, and closing it needs about five minutes of owner time in a live session. The deny rules now cover those paths regardless of what the agent files say, so the residual risk is bounded in the meantime: an agent following the stale instruction would be denied rather than leak.
Recommend this stays open until the two agent files are migrated, then closes.
**Enacted for every caller an unattended run is permitted to touch. Unattended run 2026-08-28, three days before the 2026-08-31 date.**
The helper scripts were already built and correct; what had never happened was migrating the callers. Roughly fifteen inline token reads remained. They are now down to three files, and those three are blocked by design rather than by effort.
**Migrated to the helper:**
- the `gitea`, `deploy-check`, `sprint-start` and `board` skills — every `curl` in all four now goes through `~/bin/drb-gitea`
- `.claude/scheduled/checkin.md`
- `CLAUDE.md`'s issue-tracking section, which was still documenting the old "read it into a shell variable" pattern as the house style. That one mattered most: it was the instruction every future session would have followed.
**The deny rules ruled in this decision but never added are now in `.claude/settings.json`** — `cat`, `head`, `tail`, `sed` and `grep` are denied against all three credential paths, plus `Read` on each, following the precedent already set for `gcp-key.json`. `Bash(cat:*)` stays allowed generally; the three paths are now carved out of it specifically. That is the control that was actually missing on 2026-08-24: the leak was a *permitted* `cat`, so the fix has to be a path-level deny, not a tool-level one. JSON validated.
**Remaining, and it needs an attended session — three inline reads in agent definitions:**
- `.claude/agents/ceo.md:93`
- `.claude/agents/checkin.md:42`
- `.claude/agents/checkin.md:64-65, 68`
`drb-worksession.md` forbids an unattended run from writing to `.claude/agents/` at all (enacting minutes #62 decision 1, per #68). That prohibition exists to stop a runner inventing personas, and it is doing its job here even though the edit in question is a two-line safety improvement — the rule is worth more than the exception.
**So: this decision is enacted apart from three lines in two files, and closing it needs about five minutes of owner time in a live session.** The deny rules now cover those paths regardless of what the agent files say, so the residual risk is bounded in the meantime: an agent following the stale instruction would be denied rather than leak.
Recommend this stays open until the two agent files are migrated, then closes.
Enactment check, unattended run 2026-08-29 — PARTIALLY enacted, and the remainder is blocked. Due 2026-08-31, two days out.
The WSL helpers exist and are in use: drb-worksession.md, CLAUDE.md and .claude/agents/checkin.md all call the helper wrappers, which read the credential internally and print only the API response. That part of Decision 0 (minutes #62) is done, and this run used it exclusively — no token entered this run's context.
Two files were missed and still read a token directly into agent context:
.claude/agents/ceo.md — a command-substitution read of the Gitea token
.claude/agents/checkin.md — a direct read of the Telegram token, alongside the helper it already uses elsewhere
So the CEO agent and the check-in agent still pull a secret into their own context on every invocation. That is precisely the control this decision exists to establish, and it is not established while those two remain.
What blocks the fix: nothing technical — it is two find-and-replace edits. It is blocked by design. drb-worksession.md, enacting minutes #62 Decision 1 per #68, lists "never add or edit an agent definition in .claude/agents/" as a hard constraint on unattended runs. The board skill's HIRING flow says the same. This run did not attempt it and will not.
Who must unblock it: the owner, in a live attended session with Claude. Estimated under 10 minutes — smaller than the #78 persona wiring already queued for the same kind of session, and the two could be done back to back in one sitting.
Consequence if it slips past 2026-08-31: the decision is a CTO board commitment with a stated date, and it misses. The practical exposure in the meantime is bounded but real — the one demonstrated credential leak this project has had reached a transcript in exactly this manner, which is the whole reason for the decision.
**Enactment check, unattended run 2026-08-29 — PARTIALLY enacted, and the remainder is blocked. Due 2026-08-31, two days out.**
The WSL helpers exist and are in use: `drb-worksession.md`, `CLAUDE.md` and `.claude/agents/checkin.md` all call the helper wrappers, which read the credential internally and print only the API response. That part of Decision 0 (minutes #62) is done, and this run used it exclusively — no token entered this run's context.
**Two files were missed and still read a token directly into agent context:**
- `.claude/agents/ceo.md` — a command-substitution read of the Gitea token
- `.claude/agents/checkin.md` — a direct read of the Telegram token, alongside the helper it already uses elsewhere
So the CEO agent and the check-in agent still pull a secret into their own context on every invocation. That is precisely the control this decision exists to establish, and it is not established while those two remain.
**What blocks the fix:** nothing technical — it is two find-and-replace edits. It is blocked **by design**. `drb-worksession.md`, enacting minutes #62 Decision 1 per #68, lists *"never add or edit an agent definition in `.claude/agents/`"* as a hard constraint on unattended runs. The `board` skill's HIRING flow says the same. This run did not attempt it and will not.
**Who must unblock it:** the **owner**, in a live attended session with Claude. Estimated **under 10 minutes** — smaller than the #78 persona wiring already queued for the same kind of session, and the two could be done back to back in one sitting.
**Consequence if it slips past 2026-08-31:** the decision is a CTO board commitment with a stated date, and it misses. The practical exposure in the meantime is bounded but real — the one demonstrated credential leak this project has had reached a transcript in exactly this manner, which is the whole reason for the decision.
Refs #62 Decision 0, #68, #78.
.claude/agents/checkin.md:42 -> ~/bin/drb-gitea GET "/repos/logan/server-26/issues?state=open&limit=50"
.claude/agents/checkin.md:64-68 -> helper call; the two credential paths are no longer named as things to read into shell variables.
Acceptance met: a grep for telegram-bot-token / gitea-token / telegram-chat-id across .claude/ and CLAUDE.md now returns only (a) the deny entries in .claude/settings.json, and (b) two prose lines stating that the helper reads them internally. No cat-into-variable construction survives anywhere.
No commit SHA: Version 5C/.claude/ is not under version control (no git repo at the Version 5C root), so the change is live the moment it is written. ~/bin/drb-gitea and ~/bin/drb-tg already existed and are unchanged.
Closed in a live owner session, 2026-08-30, one day inside the due date.
The last three lines an unattended run was barred from touching are replaced:
- `.claude/agents/ceo.md:93` -> `wsl -e bash -lc '~/bin/drb-tg /tmp/ceo-tg-$$.txt'`
- `.claude/agents/checkin.md:42` -> `~/bin/drb-gitea GET "/repos/logan/server-26/issues?state=open&limit=50"`
- `.claude/agents/checkin.md:64-68` -> helper call; the two credential paths are no longer named as things to read into shell variables.
Acceptance met: a grep for `telegram-bot-token` / `gitea-token` / `telegram-chat-id` across `.claude/` and `CLAUDE.md` now returns only (a) the deny entries in `.claude/settings.json`, and (b) two prose lines stating that the helper reads them internally. No `cat`-into-variable construction survives anywhere.
No commit SHA: `Version 5C/.claude/` is not under version control (no git repo at the Version 5C root), so the change is live the moment it is written. `~/bin/drb-gitea` and `~/bin/drb-tg` already existed and are unchanged.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Decision 0, board minutes #62 (2026-08-24). Owner: CTO. Due 2026-08-31.
A Telegram bot token reached a session transcript on 2026-08-24. It was revoked and rotated the same day and the owner has confirmed the exposure is closed - this issue is not about that credential, which is done. It is about the mechanism that let it happen, and would let it happen again with a credential that matters more.
Verified during the sitting (values never printed): the token-shaped string in the one affected transcript does not match the live token; the Gitea token appears in no transcript.
Why a permission prompt is not the fix
.claude/settings.jsonallowsBash(cat:*)unconditionally. Reading a token file into context was a fully permitted action - running the runner without--dangerously-skip-permissionswould have prompted for nothing and prevented nothing. #62 Decision 1 keeps the flag for that reason and puts the control here instead.The fix
Provide WSL helper scripts that read the secret internally and never echo it:
drb-tg-send- reads~/.telegram-bot-tokenand~/.telegram-chat-id, takes the message body on stdin or as a file path, prints only the HTTP status.drb-gitea METHOD PATH [json-file]- reads~/.gitea-token, prints only the response.Then remove every
cat ~/.*-tokenconstruction and every shell substitution that puts a token value on a command line from:.claude/scheduled/drb-worksession.md.claude/scheduled/checkin.mdboardskillCLAUDE.md(the issue-tracking section documents the token-path pattern)Acceptance: a grep for
telegram-bot-tokenandgitea-tokenacross.claude/andCLAUDE.mdreturns only references to the helper scripts.Also adopted, from CISO draft #61 rec. 2
Add explicit
denyentries to.claude/settings.jsonforcat,head,tail,sed -nandgrepagainst the three token paths andServer/drb-c2-core/gcp-key.json. Note in the file that this is a speed bump for attended sessions only - it is bypassed by the flag the scheduled runner uses, and must not be described anywhere as the boundary.Refs #62, #61, #56.
Enacted for every caller an unattended run is permitted to touch. Unattended run 2026-08-28, three days before the 2026-08-31 date.
The helper scripts were already built and correct; what had never happened was migrating the callers. Roughly fifteen inline token reads remained. They are now down to three files, and those three are blocked by design rather than by effort.
Migrated to the helper:
gitea,deploy-check,sprint-startandboardskills — everycurlin all four now goes through~/bin/drb-gitea.claude/scheduled/checkin.mdCLAUDE.md's issue-tracking section, which was still documenting the old "read it into a shell variable" pattern as the house style. That one mattered most: it was the instruction every future session would have followed.The deny rules ruled in this decision but never added are now in
.claude/settings.json—cat,head,tail,sedandgrepare denied against all three credential paths, plusReadon each, following the precedent already set forgcp-key.json.Bash(cat:*)stays allowed generally; the three paths are now carved out of it specifically. That is the control that was actually missing on 2026-08-24: the leak was a permittedcat, so the fix has to be a path-level deny, not a tool-level one. JSON validated.Remaining, and it needs an attended session — three inline reads in agent definitions:
.claude/agents/ceo.md:93.claude/agents/checkin.md:42.claude/agents/checkin.md:64-65, 68drb-worksession.mdforbids an unattended run from writing to.claude/agents/at all (enacting minutes #62 decision 1, per #68). That prohibition exists to stop a runner inventing personas, and it is doing its job here even though the edit in question is a two-line safety improvement — the rule is worth more than the exception.So: this decision is enacted apart from three lines in two files, and closing it needs about five minutes of owner time in a live session. The deny rules now cover those paths regardless of what the agent files say, so the residual risk is bounded in the meantime: an agent following the stale instruction would be denied rather than leak.
Recommend this stays open until the two agent files are migrated, then closes.
Enactment check, unattended run 2026-08-29 — PARTIALLY enacted, and the remainder is blocked. Due 2026-08-31, two days out.
The WSL helpers exist and are in use:
drb-worksession.md,CLAUDE.mdand.claude/agents/checkin.mdall call the helper wrappers, which read the credential internally and print only the API response. That part of Decision 0 (minutes #62) is done, and this run used it exclusively — no token entered this run's context.Two files were missed and still read a token directly into agent context:
.claude/agents/ceo.md— a command-substitution read of the Gitea token.claude/agents/checkin.md— a direct read of the Telegram token, alongside the helper it already uses elsewhereSo the CEO agent and the check-in agent still pull a secret into their own context on every invocation. That is precisely the control this decision exists to establish, and it is not established while those two remain.
What blocks the fix: nothing technical — it is two find-and-replace edits. It is blocked by design.
drb-worksession.md, enacting minutes #62 Decision 1 per #68, lists "never add or edit an agent definition in.claude/agents/" as a hard constraint on unattended runs. Theboardskill's HIRING flow says the same. This run did not attempt it and will not.Who must unblock it: the owner, in a live attended session with Claude. Estimated under 10 minutes — smaller than the #78 persona wiring already queued for the same kind of session, and the two could be done back to back in one sitting.
Consequence if it slips past 2026-08-31: the decision is a CTO board commitment with a stated date, and it misses. The practical exposure in the meantime is bounded but real — the one demonstrated credential leak this project has had reached a transcript in exactly this manner, which is the whole reason for the decision.
Refs #62 Decision 0, #68, #78.
Closed in a live owner session, 2026-08-30, one day inside the due date.
The last three lines an unattended run was barred from touching are replaced:
.claude/agents/ceo.md:93->wsl -e bash -lc '~/bin/drb-tg /tmp/ceo-tg-$$.txt'.claude/agents/checkin.md:42->~/bin/drb-gitea GET "/repos/logan/server-26/issues?state=open&limit=50".claude/agents/checkin.md:64-68-> helper call; the two credential paths are no longer named as things to read into shell variables.Acceptance met: a grep for
telegram-bot-token/gitea-token/telegram-chat-idacross.claude/andCLAUDE.mdnow returns only (a) the deny entries in.claude/settings.json, and (b) two prose lines stating that the helper reads them internally. Nocat-into-variable construction survives anywhere.No commit SHA:
Version 5C/.claude/is not under version control (no git repo at the Version 5C root), so the change is live the moment it is written.~/bin/drb-giteaand~/bin/drb-tgalready existed and are unchanged.