Skip to content

security(sandbox): deny credential reads across native stages - #370

Merged
datacas merged 3 commits into
mainfrom
feat/294-deny-credential-reads
Oct 30, 2026
Merged

datacas merged 3 commits into
mainfrom
feat/294-deny-credential-reads

Conversation

@datacas

@datacas datacas commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Implements #294.

Native stage commands now deny SSH, AWS, Codex and Claude credential-store reads. Publishing stages retain gh access; other stages and dependency preparation deny it. Existing symlink targets resolve before masking and absent paths are omitted. Explicit code_cycle.user_choices.credential_reads exceptions are validated, carried from trusted source configuration, and recorded in stage/preparation evidence.

Linux/WSL2 dependency preparation uses installed srt for both executors, including user-local prerequisite recovery. macOS/Windows retain the Codex preparation fallback with matching denials until their srt path is validated. Sandbox startup failures retain the existing unverified/static recovery behavior and never retry preparation without confinement. Inherited Claude allowRead settings are refused because they could cancel the new denials.

Diagnosis

Classification: feature_request; decision: implement. The source confirmed the issue's mechanism: Codex stage profiles and Claude settings omitted credential denials, while preparation applied only a partial list in its srt branch. The invariant is that sandboxed commands cannot read credential stores except for explicitly authorized project exceptions and publishing gh access. This shared policy now owns path resolution and denial selection across the boundaries.

No reproduction read real credentials. The owner supplied H-03/H-05 boundary evidence on 2026-10-30 and explicitly authorized L-1/L-2. Related-work search covered linked issues, all-state credential/sandbox issue titles, and merged PRs matching those terms. Related #238 and #291 remain separate work; #279, #284, #288 and #289 provide the existing preparation, transport, recovery and permission lifecycle. This change does not implement runtime-owned publication or host execution of checks.

Work units

Unit Behaviour Files Tests Verification Rollback
WU-1 Deny credential reads consistently, preserve explicit project exceptions, and unify Linux preparation scripts/{stage_permissions,executors,dependency_setup,native_prerequisites,user_choices}.py; owning docs; regression tests; Windows shard inventory Credential/profile matrix, missing-denial rejection, configuration load and malformed opt-out, executor/preparation/permission regressions Full unittest discovery; package validator; focused suites; scoped BasedPyright comparison; live E7 matrix remains unverified independent: revert this commit to restore previous policy and preparation selection

Verification (agent-reported)

  • python3 -m unittest discover -s tests — exit 0; 2,102 tests, one host-only live-matrix skip; 296 seconds.
  • python3 scripts/validate-package.py — exit 0; 16 skills valid, shared sections and manifests aligned.
  • python3 -m unittest discover -s tests -p test_credential_reads.py — exit 0; ten tests, one live-matrix skip.
  • Final affected suites: executor 191 tests, dispatch 44, preparation 29, prerequisites 27, Git sandbox 23, fast path 42 — all exit 0.
  • git diff --check — exit 0.
  • basedpyright dependency_setup.py executors.py native_prerequisites.py stage_permissions.py user_choices.py (from scripts/) — exit 1; existing diagnostics, as described below.

The focused credential suite exercises ten tests, with the host-only live matrix skipped. The installed Codex confinement probe and srt prerequisite probe now check synthetic credential stores; the opt-in live E7 matrix is documented in docs/verification.md. It was not launched in this native stage because nested Codex/srt execution is unsupported. No live sandbox pass is claimed. macOS and Windows validation remains pending, so their Codex preparation fallback remains.

Scoped BasedPyright completed with existing diagnostics (105 errors; baseline snapshot 106). It is not a clean type-check pass. No unrelated legacy diagnostics or type-check settings were changed. Fixtures set their own Git identity and use synthetic credential homes where exact profile contents matter; unit/fake-process checks do not establish OS isolation. CI can complement the local suite but is separate evidence.

Documentation

This visible behavior was not previously documented. Updated docs/host-permissions.md, docs/dependency-preparation.md, docs/configuration.md and docs/verification.md describe the stores, exception key, evidence, platform fallback and live verification procedure. No skill or product entry point was added.

Routing supplied by runtime: profile deep_coder, requested model openai/gpt-6.1-sol, effort medium, resolved model ?.

Closes #294

@datacas

datacas commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

Implemented #294 at 50e5b985066f7986e585a9fcd7918c521499925f. Upload succeeded through the stage's publication helper, and gh pr view confirmed the PR head equals that commit.

Credential denials now share canonical path handling across Codex, Claude and preparation. Linux preparation uses srt for both executors; macOS/Windows retain the aligned Codex fallback pending platform validation. Explicit project exceptions are validated and recorded. Owning documentation and regression coverage are included.

Verification, agent-reported:

  • python3 -m unittest discover -s tests: exit 0, 2,102 tests, one host-only E7 matrix skip; 296 seconds.
  • python3 scripts/validate-package.py: exit 0.
  • python3 -m unittest discover -s tests -p test_credential_reads.py: exit 0, ten tests, one live-matrix skip.
  • Final affected executor (191), dispatch (44), preparation (29), prerequisite (27), Git sandbox (23) and fast-path (42) suites: all exit 0.
  • git diff --check: exit 0.
  • Scoped BasedPyright from scripts/: exit 1, 105 existing errors versus 106 in the baseline snapshot. No clean type-check pass is claimed.

The live native E7 boundary remains not_verified: nested Codex/srt execution is unsupported in this stage, so the host-only matrix was not launched. Unit and fake-process coverage does not establish installed OS confinement. macOS/Windows validation also remains pending. These known prerequisites do not change the completed functional status. No review or approval is claimed.

Checks read once for 50e5b985066f7986e585a9fcd7918c521499925f under parallel CI policy (0 passed, 0 failed, 5 pending):

  • Skills, manifests, and Bash installer: pending (QUEUED).
  • Windows test shard (windows-1): pending (QUEUED).
  • Windows test shard (windows-2): pending (QUEUED).
  • Windows test shard (windows-3): pending (QUEUED).
  • Windows test shard (windows-4): pending (QUEUED).

Routing supplied by runtime: profile deep_coder, requested model openai/gpt-6.1-sol, effort medium, resolved model ?.

@datacas

datacas commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

[CCR-20261030-001] · senior_reviewer · anthropic/claude-opus-5-5→? · high · schema:2

Initial review — PR #370 (work item #294)

Verdict: CHANGES_REQUESTED at 50e5b985066f7986e585a9fcd7918c521499925f.

Summary and scope

Reviewed the full accumulated diff origin/main...50e5b98 (merge base 2904cac732304f1a57139cc6db8f6c0071845cc2, 17 files, +538/−72; runtime diff file of 1225 lines, line count confirmed; its SHA-256 could not be recomputed because sha256sum is outside this stage's command table). Covered: scripts/{stage_permissions,executors,dependency_setup,native_prerequisites,user_choices}.py, the Windows shard inventory, the four docs/ pages and all test changes.

The core of #294 is sound. credential_read_denials takes one shared list of stores, resolves existing symlink targets, skips absent paths and rejects unknown opt-outs. Codex profiles now always carry ="none" rules in one filesystem table. Preparation on Linux uses srt for both executors with the same denyRead. Both the confinement probe and the srt probe now check synthetic stores, and inherited allowRead is refused. One change goes beyond the issue, though: Claude's Bash sandbox is now switched on for every Claude stage, and the preflight that guards it was not widened to match. That is the blocking finding below.

Findings

[REV-001] · high · open · - · blocks:yes — Claude sandbox now forced on every stage, including inactive (non-GitHub) policies, without the bwrap preflight or static-only fallback

Paths: scripts/executors.py:2842, scripts/executors.py:1293, scripts/executors.py:1331, scripts/stage_permissions.py:1866, docs/host-permissions.md:21

ClaudeAdapter.policy_files used to add review_sandbox_settings only for an active policy that needed it (confined verification, registry domains, publication commands or Docker). It now adds it whenever not runtime_policy.static_only. That includes inactive policies (Bitbucket code host, or Plane/Jira issue providers) and active roles such as issue_review. The sandbox it adds is failIfUnavailable: true, allowUnsandboxedCommands: false, and limits the network to REVIEW_SANDBOX_DOMAINS plus registry domains.

The guard was not widened with it. The Linux bwrap/socat functional probe and the switch to static_only (executors.py:1293-1309) still run only when needs_bash_sandbox, which needs an active policy plus verification, registry or publication. The result:

  • Inactive cycles change behaviour. A Claude implementer on Bitbucket, or on any Plane/Jira cycle, used to run with the executor's normal tooling. docs/host-permissions.md:21-24 and StagePolicy.inactive_reason still promise that ("keep the earlier behaviour" / "keeps the executor's normal tooling"). Now its Bash cannot reach hosts outside the allowlist, and raw SSH (git push to a Bitbucket SSH remote, which security(sandbox): deny credential reads in every stage and preparation sandbox; one preparation sandbox tool #294 itself says is never proxied) cannot work. Masking ~/.ssh adds a second barrier. Opting out with credential_reads: ["~/.ssh"] does not help, because the docs state the HTTPS proxy still blocks raw SSH. So no configuration lets these stages push.
  • No fallback when the sandbox is unavailable. For those stages, a host with missing or broken bwrap/socat now fails closed at Claude start-up (SANDBOX_UNAVAILABLE_MARKERS). The existing recovery policy expects static_only/not_verified instead, because the probe that would choose static_only never runs.
  • policy_preflight now also runs for inactive Claude policies (executors.py:1331), so user settings that widen the sandbox refuse non-GitHub dispatches that used to be accepted.

Evidence: tests/test_stage_dispatch.py:418 (test_an_inactive_policy_keeps_the_earlier_tooling_and_says_so) was changed in this PR to assert {"hooks", "env", "sandbox"} for a Bitbucket implement + push_branch dispatch. It passes in the local run, so the runtime really emits the sandbox for an inactive policy. The issue scoped the change to the profiles that already exist ("in review_sandbox_settings (denyRead)") and did not ask to switch the sandbox on for stages that had none. The docs page that owns the inactive-policy behaviour was not updated.

Expected: either keep the earlier emission condition and add credential denyRead only where a sandbox already applied, or widen needs_bash_sandbox/static-only recovery to the same set of stages. Then document the new inactive-policy confinement (including that SSH remotes cannot push) in docs/host-permissions.md. If the owner wants inactive stages confined, that is a scope decision for #294 and should be recorded there.

[REV-002] · medium · open · - · blocks:no — Issue-review stages lose gh credentials by default; Claude case undocumented and #279 comment now stale

Paths: scripts/executors.py:1421, scripts/stage_permissions.py:532, docs/dependency-preparation.md:174

issue_review is not a publishing stage, so ~/.config/gh is now denied by default on both executors. Its command table (GH_TABLE: gh issue view/list, gh search issues/prs) relies on authenticated gh to read the work item and related work. This follows the issue text ("Keep ~/.config/gh readable only in roles that publish"), and the docs give Codex issue review the credential_reads: ["~/.config/gh"] exception. The gaps:

  • Claude issue review (which now gets a sandbox because of REV-001) loses gh the same way, and no docs page mentions it.
  • The code comment at executors.py:1421-1424 still says issue review uses "the same credentials as publishing stages (Keep development moving through routine sandbox and dependency setup #279)", which is no longer true.
  • By default, an issue_review: auto dispatch on a private GitHub repository can no longer read its own issue. The likely outcome is a spurious BLOCKED/NEEDS_REFINEMENT rather than a clear configuration error.

Code inspection only; not reproduced against a live gh (it would need nested sandbox execution). Suggest documenting the default for both executors next to the issue-review configuration and correcting the comment, or confirming in #294 that the #279 grant is being withdrawn on purpose.

Security triage

The deterministic rule did not fire: .code-cycle.yml declares no security_review.always_when, and no changed path matches the mandatory minimum (CI, secrets/keys, agent/toolkit configuration). Reviewer judgement activated the audit, because the change is sandbox and credential-confinement security tooling. The audit ran inline in this review (no separate cc-security-review pass).

Result: no confirmed vulnerability. The denials narrow access and widen nothing for GitHub stages, and the opt-out is a closed list validated in user_choices.load and credential_read_denials. REV-001 is a functional regression or scope change, not a widening. Residual risks are listed below.

Commands executed

Command Result
python3 -m unittest discover -s tests exit 0 — Ran 2102 tests in 206.715s, OK (skipped=1) (the host-only live E7 matrix)
python3 scripts/validate-package.py exit 0 — VALID: 16 skills (7 cycle, 9 supporting), shared sections in sync, cross-agent manifests aligned
python3 -m unittest exit 0 — Ran 0 tests: from the repository root it discovers nothing, so it is not evidence

Unverified

  • Live E7 matrix (CODE_CYCLE_LIVE_CREDENTIAL_PROBE=1): not run. This stage runs inside a native sandbox, and nested Codex/srt is unsupported. Real OS-level denial is not_verified here.
  • A dedicated reproduction for REV-001 beyond the existing passing test was not written: this stage's policy allows temporary files only for the comment, report and check logs. REV-001 rests on code inspection plus the passing assertion in tests/test_stage_dispatch.py:433.
  • macOS/Windows preparation fallback: unit-tested only (by mocking sys.platform).

CI (head 50e5b98, read once under the parallel policy)

Check State
Skills, manifests, and Bash installer fail (5m28s)
Windows test shard (windows-2) fail (8m24s)
Windows test shard (windows-1) pending
Windows test shard (windows-3) pending
Windows test shard (windows-4) pending

Two required checks already fail on this head. The cycle's CI gate decides on them; they are not counted as review findings.

Residual risks

Project review verdict: CHANGES_REQUESTED — REV-001 blocks approval.

@datacas

datacas commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

[CCR-20261030-001] · senior_reviewer · anthropic/claude-opus-5-5→? · high · schema:2

[CCT-20261030-001] · deep_coder · openai/gpt-6.1-sol→? · high · triaged:50e5b985066f7986e585a9fcd7918c521499925f · schema:2

BLOCKED — both review findings are resolved; the separate supplied CI failure cannot be diagnosed because this stage's sandbox blocks its log endpoints.

Fix commit: b06504281eae89664a8305727ab6e5142ee41349. Stage-local publication preflight and upload succeeded. gh pr view confirmed this exact PR head, and the worktree is clean. No approval is claimed.

[REV-001] · high · resolved · valid · blocks:yes — Claude sandbox now forced on every stage, including inactive (non-GitHub) policies, without the bwrap preflight or static-only fallback

Restored the previous Claude sandbox scope: active GitHub policies requiring confined review verification, registry domains, publication commands or Docker. Inactive Bitbucket/Plane/Jira policies retain their earlier tooling and no longer receive sandbox configuration refusals.

StagePolicy.uses_bash_sandbox selects emission, configuration checks and Linux functional preflight. The final policy is checked again after preparation adds registry domains, including incomplete preparation. Failed isolation selects static-only recovery; earlier functional probes are reused. The delegated review caught the delayed-activation edge and it was fixed before publication.

Regression coverage verifies inactive providers, ordinary issue review, Docker-only activation, inherited allowRead refusal, prepared/incomplete setup and static fallback. Final dispatch suite: 49 passing tests. Applied in the fix commit above; real OS confinement remains unverified as described below.

[REV-002] · medium · resolved · valid · blocks:no — Issue-review stages lose gh credentials by default; Claude case undocumented and #279 comment now stale

Corrected the #279 comment to distinguish network access from credential access. Issue-review configuration now documents both executors: Codex denies stored gh credentials in this non-publishing role; ordinary Claude issue review retains earlier tooling, while sandboxed Claude issue review also needs credential_reads: ["~/.config/gh"] for stored-credential authentication. An access failure does not mean the work item needs refinement. The exception grants no publication authority.

Updated the owning pages docs/configuration.md, docs/dependency-preparation.md and docs/host-permissions.md in the fix commit. The PR description already identifies these owning pages. Both dispositions were frozen as valid against the triage SHA before edits. No finding was rejected or left open; there were no native review threads.

Verification (agent-reported)

Command Result
python3 -m unittest discover -s tests Final code: exit 0; 2,107 tests in 195.432s, one host-only skip
python3 scripts/validate-package.py Exit 0; 16 skills valid, shared sections/manifests aligned
python3 -m unittest discover -s tests -p test_stage_dispatch.py Exit 0; 49 tests; delegated reviewer independently passed the same suite
python3 -m unittest discover -s tests -p test_credential_reads.py Exit 0; 10 tests, one host-only skip
python3 -m unittest Exit 0; zero tests from repository root; not meaningful suite evidence
basedpyright executors.py stage_permissions.py (from scripts/) Exit 1; 87 errors, 1,342 warnings. The preceding local run had the same 87 errors; helper extraction adds no errors and two explicit-Any warnings. No clean type-check pass claimed
git diff --check; git diff --cached --check Both exit 0

Fixtures use synthetic credential homes and their own Git identity. Local installed tools, user configuration and OS differ from CI. Unit/fake-process checks do not establish kernel confinement; preparation evidence is separate from locally executed tests.

Live E7 remains not_verified: nested Codex/srt is unsupported in this native stage, so the host-only matrix was skipped. macOS/Windows confinement was not exercised. Verification conclusion: not_verified, boundary required, retaining the successful local results above.

Post-change review and security

Delegated cc-code-review and cc-security-review covered the complete fix delta from trusted review base 50e5b985066f7986e585a9fcd7918c521499925f, confirmed as an ancestor. The final delta has no remaining confirmed findings. No changed path matched the mandatory security minimum and the PR had no sensitive labels; judgement activated the audit for credential/confinement tooling.

Reviewed activation, inherited overrides, credential scope, inactive providers and static-only propagation. Actual OS isolation and installed Claude settings merging remain unverified. Inactive Claude policies intentionally retain their earlier tooling and credential access, as documented.

Separate supplied CI failure — unresolved diagnosis

The supplied failure is on 50e5b985066f7986e585a9fcd7918c521499925f: run 41054876202, check run/job 124059735030, Skills, manifests, and Bash installer. GitHub metadata identifies Validate skills and package metadata as the failed step. Annotations contain exit code 1 and a Node 20 deprecation warning, without the diagnostic cause.

While that SHA was still the current PR head, these read-only attempts ran (each with the repository's rtk prefix):

  • gh run view 41054876202 --repo datacas/code-cycle-toolkit --log-failed: initially reported the workflow still in progress.
  • gh run view 41054876202 --repo datacas/code-cycle-toolkit --job 124059735030 --log-failed: initially the same condition; later blocked at results-receiver.actions.githubusercontent.com by this sandbox's network allowlist.
  • gh api repos/datacas/code-cycle-toolkit/check-runs/124059735030/annotations: succeeded, with the limited annotations above.
  • gh api repos/datacas/code-cycle-toolkit/actions/jobs/124059735030/logs: blocked at productionresultssa10.blob.core.windows.net by the same sandbox policy.

gh is available and authenticated. This is an observed network restriction; no permission bypass was attempted. The available evidence does not establish an in-scope defect or infrastructure cause. Local passes do not resolve that CI failure, and the resolved findings do not hide it. Resume diagnosis through an execution capability authorized to retrieve code-host logs, retaining this branch and commit.

Checks of the pushed head — one read, parallel policy

Head b06504281eae89664a8305727ab6e5142ee41349: 0 passed, 0 failed, 5 pending:

  • Skills, manifests, and Bash installer: pending (QUEUED).
  • Windows test shard (windows-1): pending (QUEUED).
  • Windows test shard (windows-2): pending (QUEUED).
  • Windows test shard (windows-3): pending (QUEUED).
  • Windows test shard (windows-4): pending (QUEUED).

The downstream PowerShell installer had not yet been reported. No remote-CI wait or in-stage CI fix round ran. These new checks do not establish the supplied old-head failure's cause.

Linux dependency preparation now runs srt for both executors, so the
preparation tests resolved the host's srt and failed on a runner without
it. Patch the resolver with a fake srt by default; the managed-rg test
keeps the real resolver because it supplies its own bundle.

The emulated sandbox in the confinement-probe test compared canonical
paths with unresolved table roots, so a temporary root reached through a
symlink or a Windows 8.3 short name denied the clone and TMPDIR writes.
Compare against resolved roots and cover a linked temporary root.

Refs #294
@datacas

datacas commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

[CCR-20261030-001] · senior_reviewer · anthropic/claude-opus-5-5→? · high · schema:2

[CCT-20261030-001] · deep_coder · openai/gpt-6.1-sol→? · high · triaged:50e5b985066f7986e585a9fcd7918c521499925f · schema:2

[CCT-20261030-002] · deep_coder · anthropic/claude-opus-5-5→? · high · triaged:b06504281eae89664a8305727ab6e5142ee41349 · schema:2

RESOLVED. This attempt resumes the earlier resolve round, which was BLOCKED. Both review findings stay resolved and were re-verified at the new head. The CI failures on b065042 were traced to two test-environment dependencies and fixed in test-only commit 533ab04a38e14e2f017ff307303fd595275858d8. The publication helper uploaded it, and gh pr view confirmed it is the PR head. No approval is claimed.

[REV-001] · high · resolved · valid · blocks:yes — Claude sandbox now forced on every stage, including inactive (non-GitHub) policies, without the bwrap preflight or static-only fallback

Fixed in b06504281eae89664a8305727ab6e5142ee41349 by the previous resolve attempt; the disposition valid is unchanged. StagePolicy.uses_bash_sandbox restores the pre-#294 emission scope. The configuration check and the Linux bwrap preflight use the same condition, and the final policy is checked again after preparation. Re-verified at 533ab04: test_stage_dispatch.py (49 tests) passes inside the full suite. Those tests cover inactive Bitbucket/Plane/Jira policies, ordinary issue review, Docker-only activation and static-only fallback.

[REV-002] · medium · resolved · valid · blocks:no — Issue-review stages lose gh credentials by default; Claude case undocumented and #279 comment now stale

Fixed in b06504281eae89664a8305727ab6e5142ee41349; the disposition valid is unchanged. The #279 comment now separates network access from credential access. docs/configuration.md, docs/dependency-preparation.md and docs/host-permissions.md document the issue-review default for both executors and the credential_reads: ["~/.config/gh"] exception. Re-checked against the current head; no further change was needed.

CI failures on b065042 (not review findings)

On run 41058014313, three checks failed: Skills, manifests, and Bash installer, Windows test shard (windows-2), and PowerShell installer. The PowerShell installer fails by design when any Windows shard fails. main (2904cac) passes the same workflow, so this branch caused the failures.

The failure logs could not be read. gh run view 41058014313 --job 124070332572 --log-failed was refused at results-receiver.actions.githubusercontent.com (the sandbox's network policy, user denied). The check-run annotations hold only Process completed with exit code 1. I therefore reproduced the failures locally, and found two causes:

  1. Linux CI depended on the host having srt. Linux preparation now runs srt for both executors, and 18 tests in tests/test_dependency_setup.py looked up the host's srt. With srt made unavailable, 14 tests failed and 4 errored with dependency preparation needs the installed srt sandbox runtime. Fix: setUp now supplies a fake srt. The managed-rg test keeps the real lookup, because it ships its own bundle. Two tests now patch executable directly instead of shutil.which.
  2. The emulated sandbox broke on linked temporary paths (windows-2 shard and Linux). The emulated sandbox in test_confinement_probe_rejects_each_missing_credential_denial resolved paths before matching them, but compared them against table roots that were not resolved. When the temporary root sits behind a symlink, or behind a Windows 8.3 short name (RUNNER~1), writes to the clone and TMPDIR were denied. The new regression test test_confinement_probe_emulation_follows_a_linked_temporary_root failed before the fix with did not confine … clone; TMPDIR and passes after it. The emulator now compares against resolved roots.

These are reproduced failure modes that match both failing environments. Without the logs, they are not confirmed as the exact CI causes. The checks of the new head will settle it.

Verification (agent-reported)

Command Result
python3 -m unittest discover -s tests exit 0; Ran 2108 tests in 213.734s, OK (skipped=1); the skip is the host-only live E7 matrix
python3 scripts/validate-package.py exit 0; VALID: 16 skills (7 cycle, 9 supporting), shared sections in sync, cross-agent manifests aligned
python3 -m unittest exit 0; Ran 0 tests from the repository root, so not evidence
python3 -m unittest discover -s tests -p test_credential_reads.py before the fix: exit 1 (the new linked-root test fails); after: exit 0, 11 tests, 1 skip
python3 -m unittest discover -s tests -p test_dependency_setup.py with srt simulated absent: exit 1 (14 failures, 4 errors); after the fix: exit 0, 29 tests
git diff --check exit 0

What the local run shares with CI: the same commands, synthetic credential homes, and fixtures that set their own Git identity. What it does not share: the operating system (WSL2 Linux here, not the self-hosted runner or Windows), the installed tools, and the temporary path layout.

The live E7 boundary is not_verified. Nested Codex/srt is unsupported inside this native stage, so real OS confinement was not exercised. macOS and Windows confinement remain unverified. Verification conclusion: not_verified, boundary required. The local results above still stand.

Post-change review and security

The delta base is the trusted review receipt 50e5b985066f7986e585a9fcd7918c521499925f, and it is an ancestor of the head. A delegated cc-code-review covered the complete delta from that base, including this attempt's test changes. It found no P0 or P1. It confirmed two lower-severity items in the earlier, already-pushed delta. They are recorded as residual risks below, not fixed in this round.

No changed path matches the mandatory security minimum, and the PR has no sensitive labels. This round changed tests only. The earlier attempt audited the credential and confinement code paths in b065042.

Residual risks

  • P2: credential_read_policy in the dispatch evidence (scripts/executors.py:1293) lists denied paths and gh_readable: false even for Claude stages that get no Bash sandbox, such as ordinary issue review or a review without verification commands. In those stages the denials are not enforced. The same misreport already existed for inactive and static-only policies. A follow-up should report enforcement from the final policy.
  • P3: for a Claude review that has no verification commands, the sandbox comes into scope only when preparation adds registry domains. An inherited sandbox.filesystem.allowRead is therefore refused after preparation has run, not before (scripts/executors.py:1433). The refusal is correct; only setup time is wasted.
  • Stores outside the security(sandbox): deny credential reads in every stage and preparation sandbox; one preparation sandbox tool #294 list stay readable, the WSL2 linked-key case remains, and publishing stages keep gh credentials (Make provider capabilities first-class and close 0.6.0 workflow gaps #238). All three are as stated in the review.

Checks of the pushed head (one read, parallel policy)

Head 533ab04a38e14e2f017ff307303fd595275858d8: 0 passed, 0 failed, 0 pending. No checks were registered yet at the single read straight after the push. No remote-CI wait or in-stage CI fix round ran.

Routing supplied by runtime: profile deep_coder, requested model anthropic/claude-opus-5-5, effort high, resolved model ?.

@datacas

datacas commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

[CCR-20261030-001] · senior_reviewer · anthropic/claude-opus-5-5→? · high · schema:2

[CCT-20261030-001] · deep_coder · openai/gpt-6.1-sol→? · high · triaged:50e5b985066f7986e585a9fcd7918c521499925f · schema:2

[CCT-20261030-002] · deep_coder · anthropic/claude-opus-5-5→? · high · triaged:b06504281eae89664a8305727ab6e5142ee41349 · schema:2

[CCR-20261030-002] · senior_reviewer · anthropic/claude-opus-5-5→? · high · schema:2

Rereview — PR #370 (work item #294)

Verdict: APPROVED at 533ab04a38e14e2f017ff307303fd595275858d8. Both earlier findings are fixed and were verified by execution, and all six checks of this head passed. One new low, non-blocking finding is listed below.

Scope

I rereviewed the whole accumulated diff origin/main...533ab04: merge base 2904cac732304f1a57139cc6db8f6c0071845cc2, 17 files, +724/−91. The runtime's diff file has 1,469 lines, and I confirmed that count. I could not recompute its SHA-256, because sha256sum is not in this stage's command table.

Changes since the initial review (50e5b98):

  • b065042 restores the earlier Claude sandbox scope. It adds StagePolicy.uses_bash_sandbox, a shared _bash_sandbox_preflight, and a second preflight after preparation. It also updates the issue-review docs and the Keep development moving through routine sandbox and dependency setup #279 comment.
  • 533ab04 changes tests only: a fake srt in test_dependency_setup.py, and resolved roots in the emulated confinement probe.

Previous findings

[REV-001] · high · resolved · valid · blocks:yes — Claude sandbox now forced on every stage, including inactive (non-GitHub) policies, without the bwrap preflight or static-only fallback

Corrected, and verified by execution. StagePolicy.uses_bash_sandbox (scripts/stage_permissions.py:252) is active and not static_only and (confined verification | registry domains | publication commands | docker). That is exactly the emission condition origin/main had in ClaudeAdapter.policy_files. Three things now use it: sandbox emission (executors.py:2866), the sandbox_override check (executors.py:2801), and the Linux bwrap probe with its static-only fallback (executors.py:1236).

A pre-existing gap is also closed. When preparation adds registry domains, the sandbox becomes active late; the final policy is now probed and checked again before emission (executors.py:1423-1441), and a probe that already ran is reused. Inactive Bitbucket, Plane and Jira dispatches emit only {"hooks", "env"}, as before #294. docs/dependency-preparation.md and docs/host-permissions.md now describe that scope.

The relevant tests pass in python3 -m unittest discover -s tests -p test_stage_dispatch.py -v (exit 0, 49 tests):

  • test_an_inactive_policy_keeps_the_earlier_tooling_and_says_so
  • test_inactive_policies_do_not_gain_sandbox_configuration_preflight
  • test_issue_review_sandbox_is_only_added_and_checked_when_required
  • test_docker_only_sandbox_uses_functional_preflight_and_static_fallback
  • test_preparation_registry_grant_gets_final_sandbox_configuration_check
  • test_preparation_registry_grant_gets_functional_probe_and_static_fallback
  • test_claude_unavailable_isolation_keeps_static_guards_and_pending_tests

[REV-002] · medium · resolved · valid · blocks:no — Issue-review stages lose gh credentials by default; Claude case undocumented and #279 comment now stale

Corrected. The comment at executors.py:1443-1446 now separates the #279 network grant from stored gh credentials. docs/configuration.md (issue-review section) documents the default for both executors and the credential_reads: ["~/.config/gh"] exception. It also says that an access failure does not mean the work item needs refinement.

Ordinary Claude issue review keeps its earlier tooling, which test_issue_review_sandbox_is_only_added_and_checked_when_required covers. Codex issue review still denies the gh store, as #294 intends and as now documented. I did not reproduce this against a live private repository, which would need nested sandbox execution.

New findings

[REV-003] · low · open · - · blocks:no — Dispatch evidence reports credential denials that a Claude stage without a Bash sandbox does not enforce

Paths: scripts/executors.py:1292-1297

policy_note["credential_read_policy"] is computed for every dispatch with a command policy, before any sandbox decision. It lists denied_paths and gh_readable: false even for Claude stages that get no Bash sandbox: inactive policies, static-only recovery, ordinary issue review, or a review without verification commands. In those stages the listed paths are not denied, so the stage evidence overstates confinement. The resolver noted this as a residual P2.

Found by code inspection: the note is built unconditionally, while emission is gated by uses_bash_sandbox at executors.py:2866. It does not block. Enforcement itself is correct, and docs now state where Claude applies the denials. A follow-up should derive the field from the final policy, for example by adding "enforced": runtime_policy.uses_bash_sandbox for Claude.

Security triage

The deterministic rule did not fire. .code-cycle.yml declares no security_review.always_when, no changed path matches the mandatory minimum (CI, secrets/keys, agent/toolkit configuration), and the PR has no labels. I activated the audit by judgement, because this change is credential-confinement tooling, and ran it inline.

Result: no confirmed vulnerability. Compared with origin/main, the fix delta returns Claude's sandbox to its earlier scope and widens nothing. Codex profiles and preparation keep the denials. The credential_reads opt-out is still a closed list, validated on load. The sandbox_override refusal now applies to every sandboxed Claude stage, not only confined verification. That is stricter, because an inherited allowRead could cancel the denials.

Verification (executed in this rereview)

Command Result
python3 -m unittest discover -s tests exit 0: Ran 2108 tests in 193.962s, OK (skipped=1) (the host-only live E7 matrix)
python3 scripts/validate-package.py exit 0: VALID: 16 skills (7 cycle, 9 supporting), shared sections in sync, cross-agent manifests aligned
python3 -m unittest exit 0: Ran 0 tests. Discovery from the repository root finds nothing, so this is not evidence
python3 -m unittest discover -s tests -p test_stage_dispatch.py -v exit 0: Ran 49 tests, OK, including the REV-001/REV-002 regressions listed above

Unverified:

  • The live E7 matrix (CODE_CYCLE_LIVE_CREDENTIAL_PROBE=1) did not run. This stage runs in a native sandbox, and nested Codex/srt is unsupported there, so real OS-level denial is not_verified.
  • macOS/Windows preparation fallback is covered only by unit tests that mock the platform.

CI (head 533ab04, read once, parallel policy)

Check State
Skills, manifests, and Bash installer pass
Windows test shard (windows-1) pass
Windows test shard (windows-2) pass
Windows test shard (windows-3) pass
Windows test shard (windows-4) pass
PowerShell installer pass

Six passed, none failed, none pending. These are the same jobs that failed on 50e5b98 and b065042, so the test-only commit 533ab04 resolved the CI failures that had no logs.

Residual risks

Project review verdict: APPROVED. No open finding blocks approval, and every check of the head passed.

@datacas
datacas merged commit a3d98d5 into main Oct 30, 2026
6 checks passed
@datacas
datacas deleted the feat/294-deny-credential-reads branch October 30, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(sandbox): deny credential reads in every stage and preparation sandbox; one preparation sandbox tool

1 participant