Runner must never hold a secret in its context - replace every token read with a WSL wrapper #63
Closed
opened 2026-08-24 23:41:26 -04:00 by logan
·
3 comments
No Branch/Tag Specified
Labels
Clear labels
area/ai-pipeline
area/auth
area/billing
area/correlation
area/discord
area/frontend
area/governance
area/growth
area/infra-deploy
area/legal
area/map
area/mqtt
area/node-ota
area/osint
area/recording-audio
area/sdr-radio
area/transcription
board
board:agenda
minutes:draft
minutes:final
role:ceo
role:cfo
role:cio
role:ciso
role:cmo
role:coo
role:cto
Scene/entity extraction, summarizer, AI providers, embeddings
AuthN/Z, roles, tenancy/org scoping, API keys, credentials
Plans, entitlements, AI spend accounting and caps
Incident correlation, linking, lifecycle, severity, corr_debug
Discord bot, radio bots, voice streams, bot tokens, webhooks
Web UI: Next.js app and the edge-node local dashboard
Board process, minutes, unattended runner, agent tooling
Go-to-market, outreach, cold email, discovery conversations
CI/CD, VM, registry, Firestore rules/indexes deploy, Terraform/Caddy
ToS, retention, licences/DAAs, CAN-SPAM, E&O, name suppression
Live map: pins, paths, tiles, legend, geocoding
Node-C2 MQTT, checkins, commands, node sweeper, dynsec
Node install, enrollment, OTA updater, image skew
External feeds: cameras, 511, ADS-B, AIS, outages, and their licences
Call capture, boundaries, PulseAudio, ring buffer, trimming, attribution
OP25 decode, SDR hardware/tuning, conventional, second SDR, mesh radio
STT, transcript correction, place verification, name accuracy
Board process: minutes and board-filed items
Needs board scoping at the next sitting; CEO removes the label once scoped
One exec pre-read, closed by CEO
CEO ruling; open until enacted
Filed by the CEO
Filed by the CFO
Filed by the CIO
Filed by the CISO
Filed by the CMO
Filed by the COO
Filed by the CTO
kind
behavior-bug
Wrong product behavior; the instrument reads it correctly
kind
measurement-bug
Corrupts the recording/instrument (debug data, evals, health checks), not behavior
priority
p0
Drop everything: prod broken, live exposure, or owner-ruled P0
priority
p1
Next up: blocks a gate, a customer, or core incident quality
priority
p2
Normal: real defect or planned work, schedule in turn
priority
p3
Low: minor, cosmetic, or someday
status
blocked
Waiting on an external party, spend, or another issue
status
in-progress
Branch or PR open against it
status
needs-decision
Waiting on an owner or board ruling before work
status
needs-info
Root cause or repro unknown; needs a reading first
type
bug
Something built is wrong: code, data, or a measurement
type
chore
Maintenance, refactor, tests, pinning, cleanup: no user-visible change
type
decision
A ruling to make or record: board minutes, owner calls, legal reviews
type
docs
Docs, runbooks, CLAUDE.md, skills, SOPs
type
feature
New capability or an extension of an existing one
type
security
Security, privacy, credential or tenant-isolation exposure
type
tracking
Tracker, checklist, initiative or punch-list spanning several changes
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: logan/server-26#63
Reference in New Issue
Block a user
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.