CB-596: CB-592 blocks one credential name out of about thirty that a member's login shell re-sources #82
Closed
opened 2026-08-16 16:54:38 +02:00 by ltms
·
6 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
1.1 — single-host close-out
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#82
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.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-08-16 while working #79's criterion 1. #79 asks whether a member may hold
CONTEXT7_TOKENandAI_GATEWAY_TOKEN. Answering it properly showed the question is scoped too small.What is actually true
A member runs in a herdr pane. That pane starts a login shell, and the login shell sources
${SHARED_ENV}/tools/secrets.sh. This is not a guess — it is the reason CB-592 exists in the shape it does. A plain env overlay was not enough, because the login shell re-exports the value afterwards, so CB-592 had to add theBRIDGED_MEMBERmarker and a guardedexportto win against it (HerdrPeerLauncher.java:806-819).That mechanism is name-by-name. CB-592 blocks exactly one name:
secrets.shexports about thirty. Names only, never values:The argument that justified CB-592 was: a member should not hold the operator's admin forge credential. That argument does not stop at Gitea. It applies word for word to at least these:
GITLAB_PERSONAL_ACCESS_TOKENCF_API_TOKENTS_AUTHKEY,TS_API_KEYGRAFANA_ADMIN_PASSWORD,BESZEL_ADMIN_PASSWORD,HW_PASSWORDTELEGRAM_BOT_TOKENHASS_TOKENCONFLUENCE_API_TOKEN,LTMS_API_KEY,BRAIN_MCP_TOKEN,MEMORY_MCP_TOKENWhat is measured and what is not — read this before acting
Measured, on 2026-08-16 (recorded on #79): a live Claude Code member held
GITEA_ACCESS_TOKENwith the blocked sentinel value, heldBRIDGED_MEMBER=1, and held a realWORKER_GITEA_TOKEN. So the inheritance path and the CB-592 block are both proven for those names.Not measured: whether the other names above are actually present in a live member's environment. The inference is strong — same shell, same file, same mechanism — but it is an inference. I attempted the probe and stopped: enumerating credential names inside a member is exactly the kind of action that should need the operator's explicit approval, and the command classifier refused it. That refusal was correct and I did not route around it.
So step 1 of this ticket is the operator authorising and running that measurement. Do not build a fix on the inference alone.
Why this is a release-1 item
It is not federation-dependent and it does not get better with time. It is also cheap to get wrong in the reassuring direction: CB-592 works, and a working block on one name reads as "credentials are handled" when twenty-nine are untouched.
Note the shape of the CB-592 failure, from #79: a member mounts 45
mcp__gitea__*tools includingdelete_branchanddelete_file, and they fail only because the credential they read is blocked. Present-and-useless looks identical to absent. Nothing in that picture would look different if the block were missing.Acceptance criteria
secrets.shlater is not silently granted to members. Either the list is deny-by-default, or something reports the gap. Say which and why.AI_GATEWAY_TOKENis treated as its own case:bridged.yamlnames it intokenEnv:for thelocalandgxprofiles, so a member reaching the gateway is by design, not by accident. Record it as required, not as a leak.wiki/11-Features.mdentry: what it does · the knob · why · the gotcha.Relationship to #79
#79 stays open for its own two criteria. This ticket is the general case its criterion 1 turned out to be a corner of. Answer this one and #79's criterion 1 falls out of it.
Not in scope
${SHARED_ENV}/tools/secrets.sh. It is the operator's file.Step 1 is now one command — it needs your go-ahead, not your time
scripts/probe-member-credentials.shis onmainas837fed7. I have not run its reading path. That is the authorisation this ticket is blocked on, and running it myself would be routing around the refusal rather than resolving it.What to run
Paste both outputs here. Lining them up is the answer to criterion 1.
What it will not do
secrets.sh— your file.BRIDGED_MEMBER=1, so it cannot be pointed at the wrong shell by accident.One deliberate change from criterion 1
The criterion asked for a 6-character prefix of each value. The script prints a truncated SHA-256 instead.
A 6-character prefix of a short secret is most of the secret, and the output of this probe is going to be pasted into a ticket. The hash answers every question the prefix was for — is it set, is it the same value as the one over there, is it the CB-592 sentinel — and answers none of the ones it should not. If you would rather have the prefix, say so and I will change it; I did not want to make that call silently.
How to read the two outputs
SHA256-12matches on both sides is a credential the member holds in full. That is the finding.GITEA_ACCESS_TOKENis the control. CB-592 replaces it with a blocked sentinel, so its hash should differ. If it matches, CB-592 is not working, and that is more urgent than everything else on this page.AI_GATEWAY_TOKENmatching is expected and correct, not a leak —bridged.yamlnames it intokenEnv:for thelocalandgxprofiles, so a member reaching the gateway is by design. Criterion 5.The limit worth knowing before you read the result
The name list was recorded on 2026-08-16 and does not update itself. Anything added to
secrets.shsince then is invisible to this probe. That is not an oversight in the script — it is the same gap criterion 4 asks to close properly, and it means a clean-looking result is only clean for the names it asked about.Where the rest of the ticket stands
Criteria 2–6 all depend on this measurement, so they wait. Once the two readings are here I can brief the list-driven blocking (criterion 3) and the deny-by-default question (criterion 4) as ordinary work.
This is now the only thing between milestone 1.1 and the tag, apart from CB-586 (#67), which is in flight.
Status: this is now the only open issue on milestone 1.1 — 19 closed, 1 open. Every code item is merged, and the daemon has been redeployed onto a jar that contains all of them (
64908de1cb56, pid 93820, 860 tests, BUILD SUCCESS). A live spawn afterwards confirmed the fleet still works.Nothing else blocks
v1.1.0. This ticket does, and it cannot close itself — the reading path is refused by the command classifier from inside a session, which is the whole point of the ticket.The ask, unchanged and one paste:
It refuses to run outside a member unless
--allow-outside-memberis passed. It prints one line per credential name —NAME | STATE | LEN | SHA256-12— and never a value, never a prefix of a value. Paste the output here and I can finish the ticket.Criterion 1 is met — the measurement is done
The operator ran
scripts/probe-member-credentials.shfrom their own shell and it refused, correctly: the finding is about what a member holds, and the script gates onBRIDGED_MEMBER=1. So I spawned a member (term_6592f25a7e87428, profilelocal, its own worktree) and delegated the probe to it throughbridge_send.It ran inside the member, exit code 0, nothing on stderr:
Every one of the ~30 names is set in a live member. The inference in the ticket body was right, and it is now measured rather than reasoned.
On the hash column — deliberately not reproduced here
The probe prints
NAME | STATE | LEN | SHA256-12. I am not pasting that table into this ticket, and the ticket's own criterion 1 ("length and a 6-character prefix") should be revised for the same reason.A 12-hex-char SHA-256 prefix is harmless for a 48-character token. It is not harmless for a short value.
GRAFANA_ADMIN_USERhas length 5 and hashes to8c6976e5b541— that is simply SHA-256 ofadmin, recoverable from any wordlist in under a second. The same applies toHW_USER(len 4),BESZEL_ADMIN_EMAIL(len 11), and most importantlyHW_PASSWORD(len 12), which is short enough to be worth attacking.So a digest is a safe comparison device between two readings on one machine, and an unsafe thing to write into a ticket. The lengths and the set/unset states below carry the whole finding without that risk.
CB-592 works — proven, without needing a second reading
GITEA_ACCESS_TOKENis the control. I did not need the operator's comparison reading to settle it, because the blocked sentinel is a hardcoded, non-secret string inHerdrPeerLauncher.java:798-799. So I hashed it myself, the same way the script does:The member holds the sentinel, not the admin token. CB-592 survives the login shell that re-sources
secrets.sh, which is exactly what theBRIDGED_MEMBERmarker and the guardedexportwere built to do. That mechanism is sound and should be the model for the fix.I also checked what else the launcher shadows. It is one name and only one:
So nothing on the rest of the list is shadowed, defaulted, or accidentally empty. They are live values.
The finding, sorted
31 names, all set. Sorting them by what they are:
GITEA_ACCESS_TOKENAI_GATEWAY_TOKEN(named intokenEnv:forlocal/gx— criterion 5),WORKER_GITEA_TOKEN(len 40, the repo-scoped forge token a member needs to open its own PR)CONTEXT7_TOKENGITEA_HOST,OPENCODE_AUTOMODE_MODEL,TELEGRAM_CHAT_IDThat last row is the ticket. Ranked by what it opens:
GITLAB_PERSONAL_ACCESS_TOKENGITLAB_OAUTH_CLIENT_SECRETCF_API_TOKEN·CF_USER_TOKENTS_AUTHKEY·TS_API_KEYHASS_TOKENBESZEL_ADMIN_PASSWORD·BESZEL_KEY·BESZEL_UNIVERSAL_TOKENGRAFANA_ADMIN_PASSWORDHW_PASSWORDTELEGRAM_BOT_TOKENCONFLUENCE_API_TOKENLTMS_API_KEY·BRAIN_MCP_TOKEN·MEMORY_MCP_TOKEN·METRICS_PUSH_TOKENGITLAB_PERSONAL_ACCESS_TOKENis the headline. The rule this whole line of work came from was only the leader and architect may useGITEA_ACCESS_TOKEN; everyone else usesWORKER_GITEA_TOKEN. CB-592 enforced that for Gitea. A full personal access token for a different forge sits right next to it, untouched, and no ticket had ever named it. That is the same shape as CB-604 → CB-606: the instance that got fixed was not the worst one.What is left — criteria 2 to 6
Criterion 1 is done. The rest is implementation, and it is real work, not paperwork:
AI_GATEWAY_TOKENrecorded as required — settled abovewiki/11-Features.mdentryCriterion 4 is the one that decides the design, and I would argue strongly for deny-by-default: an allow-list of the four names a member actually needs, with everything else replaced by a sentinel using the CB-592 mechanism that is now proven to work. An explicit deny-list has the property this ticket was filed about — it is silently wrong the moment a new name is added to
secrets.sh, and nothing anywhere would say so.That decision belongs to the operator, since it is their secret store and their services. Asking now.
Note for the milestone: this means 1.1 is not one operator command away from done. The measurement cleared the blocker on criterion 1, and criteria 2–6 are an implementation unit that still has to land.
Decision recorded — deny-by-default
The operator chose deny-by-default over a deny-list and over report-only. So criterion 4 is settled by the policy itself rather than by a separate detector: a member gets an explicit allow-list, and every other known credential name is replaced with the CB-592 sentinel.
The allow-list, with the reason each name earns its place — this is criterion 2's decision record for the allowed bucket:
AI_GATEWAY_TOKENtokenEnv:for thelocalandgxprofiles, so a member reaching the gateway is by design (criterion 5)WORKER_GITEA_TOKENCONTEXT7_TOKENGITEA_HOSTEverything else on the measured list of 31 goes in the blocked bucket. That is 27 names, and it includes
GITEA_ACCESS_TOKEN, which stops being a hardcoded Java constant and becomes an ordinary entry in the list.Delegated as ticket
task-2to asonnetmember on branchworker/cb596-4e49ef-3. The brief carries the acceptance criteria above plus four constraints worth repeating here, because each one is a way this could go wrong:knownbounds the blast radius. The implementation blocksknownminusallow. It must not loop over the whole environment and unset what it does not recognise —PATH,HOME,SHELLandTERMlive in that same environment, and wiping them breaks the member completely.BRIDGED_MEMBERmarker plus the guardedexportis the only reason the existing block works at all.policy:at config load, in the shape CB-606 established — name the field, the bad value, the accepted set, and what would otherwise have happened. A silently-accepted typo here would turn the blocking off, which is precisely the CB-606 defect wearing different clothes.Criterion 6, the
wiki/11-Features.mdentry, stays with me:wiki/is a submodule a member cannot commit to. The member supplies the raw material and I write it.The code half is done and live. Two operator actions left.
Merged
ac40de1· 870 tests, BUILD SUCCESS · deployed, jare11160695fbe. The daemon's startup line now reads:bridge_whoami→primaryafter the restart, and a live spawn worked.Criteria
memberCredentials:block,policy: deny-by-defaultblocked-by-bridged-cb596-see-gitea-issue-82allow ∩ blocked = []wiki/11-Features.mdef84c3d,bridged.example.yaml, Roadmap01aab0bCriterion 7 is the one that matters. The config half alone proves nothing, because the launcher writes the member's environment and then the pane's login shell re-sources the secret store and overwrites it. That is why CB-592 needed a guarded export in the store, and why this needs the same.
Action 1 — rotate
GITEA_ACCESS_TOKENAlready reported and already acknowledged as "later". Recording it here so it is not lost: a lead leaked about 31 characters of it while dumping the structure of the secret store. The redaction covered
export NAME=…lines; that token is set on a line that starts with a guard, so it fell through.Action 2 — append the CB-596 block to the secret store
The file is the operator's and no session edits it. The block to append is at
scratchpad/cb596-secrets-append.sh— syntax-checked in bothzshandbash, and behaviour-tested with fake values. It is aBRIDGED_MEMBER-guarded loop that exports the sentinel over the 29 blocked names. It moves, copies and retypes no existing value.I diffed the daemon's computed blocked set against that block, name by name:
Then — the probe that closes this ticket
Spawn a member, run
scripts/probe-member-credentials.sh. Pass condition:Reminder on the probe's output: it prints
SHA256-12, which is safe for comparing two readings on one machine and unsafe to publish —GRAFANA_ADMIN_USERhas length 5 and hashes to SHA-256 ofadmin. Post names and lengths, not the hash column.The gap detector earned its place on its first spawn
It warned about two names no hand-written list had ever contained, because neither lives in the secret store:
SSH_AUTH_SOCKis required today — worktree remotes aressh://git@git.ltms.dev:2224, so without it a member cannot push. But it hands a member the operator's ssh-agent: it can sign with every key the agent holds. That is strictly broader than the repo-scoped token CB-302 built to avoid exactly this. Both are allow-listed with the reasoning written intobridged.yaml, and the real fix is filed as #110 (CB-607) on 2.0 — push over HTTPS withWORKER_GITEA_TOKEN, then block it.The lesson worth keeping: the enumeration was of a file, and the exposure is of an environment. Anything granted by a handle rather than a value is invisible to a list built by reading
secrets.sh. That is the argument for the daemon reporting the gap instead of anyone trusting the list.Verified in a live member pane. Closing.
The operator backed up
secrets.shand appended the CB-596 block on 2026-08-17. Both halves are now in place, and the result was measured — not inferred.The measurement
A member was spawned (
localprofile, own worktree) and ranscripts/probe-member-credentials.shin its own pane.SET, exit 0The five that kept their real values are exactly the allow-list:
AI_GATEWAY_TOKEN(48),CONTEXT7_TOKEN(64),GITEA_HOST(21),OPENCODE_AUTOMODE_MODEL(19),WORKER_GITEA_TOKEN(40).The two that mattered most compare equal to the sentinel string, which is a hardcoded non-secret constant, so the comparison is safe to report:
26 is not 29 — the gap, and how it was closed
The probe reported 26 blocked; the policy blocks 29. Both numbers were right, counting different sets: the probe script hardcodes its own list of 31 names and never reads
bridged.yaml, so it skipped the threeN8N_*names added toknown:after the script was written.Measured separately in the same live pane:
So 29 of 29 blocked names are confirmed blocked in a real member pane — 26 by the probe, 3 by direct measurement.
The probe's own drift is filed as #111 (CB-608). It deserves its own ticket rather than a footnote here: it is this ticket's defect in a third place, and a verification tool that under-reports fails in the worst direction, because its clean output gets taken as evidence.
Two things the run taught us
The operator's shell is untouched, which was a requirement, not a hope. Same names, same login shell, no
BRIDGED_MEMBER:GITEA_ACCESS_TOKEN40,GITLAB_PERSONAL_ACCESS_TOKEN51,CF_API_TOKEN53. The guard does what it claims.CLAUDE_CODE_MESSAGING_TOKENis unset in a member pane. It is present in the daemon's environment — that is why the gap detector saw it — but members never receive it. Allow-listing it was harmless, though not for the reason it was allowed. Recorded on #111 for whoever picks up what that token actually grants.Criteria
memberCredentials:,deny-by-defaultwiki/11-Features.mdef84c3d,bridged.example.yaml, Roadmap01aab0bStill outstanding, and deliberately not blocking this
GITEA_ACCESS_TOKEN. A lead leaked about 31 characters of it into a transcript. The operator chose to rotate later. This is not a CB-596 criterion, and it should not be lost with this ticket — worth its own reminder.SSH_AUTH_SOCKis allowed because members must push, and it outranks the repo-scoped token. On 2.0.Closing.