Skip to content

Remove the guard's here-document false positives; read after reserved words - #1048

Draft
seathatflowsinourveins wants to merge 5 commits into
mainfrom
claude/api-actions-guard-fp-20261012
Draft

seathatflowsinourveins wants to merge 5 commits into
mainfrom
claude/api-actions-guard-fp-20261012

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Parked as a draft at f91c0ac (2026-10-13) by the command center's ruling ruling-pr1048-parked-native-sandbox-20261013: no further guard-pattern rounds, main's guard stays unchanged, here-document false positives keep the script-file workaround, and the forward path is the native Claude Code sandbox's credential read-deny in its own PR.

Scope

  • What this PR changes, in one or two sentences: the secret-path guard stops refusing Python and JavaScript here-document code it read as shell lines, accepts a presence check of an inventory pointer, and lets env/printenv --help and --version through. It also reads the command after a leading bash reserved word, which no segment rule read before (command center ruling of 2026-10-12, 17:19Z; the reserved-word item was folded in at 18:34Z).
  • Base commit: 6934527e (main, after Credential inventory: sharadar slot for the survivorship-free data feed #1044).
  • Commits: a9724603 (the change), b7fc4819 (fix round 1 after Astra's full read at d2fcf5b5: two P1 regressions withdrawn, see Corrections) and d2fcf5b5, which aligns the docs with the command center's 19:35Z ruling that it, not the operator, sets the settings env pointer entries after landing (five locators moved down two lines).
  • Lane: lane:foundation
  • Owned paths touched:
    • scripts/hooks/secret_path_guard.py
    • tests/test_secret_path_guard.py
    • docs/decisions/2026-10-08-guard-hook-fails-closed.md: a note pinning its line numbers to 958a6d1a.
    • docs/secret-storage.md: the new subsection "False-positive repair and reserved words (2026-10-12)", three sentences that named form F as the only exception, and a pointer note.
    • adoption/hooks/claude/SHA256SUMS: the guard pin.
    • catalogs/foundation/upstream-surface-dispositions.json: five docs/secret-storage.md locators moved past the new subsection.
    • manifests/evidence.json: re-registration.

What changes. The guard docstrings and the docs subsection bound each item.

  • L2. A here-document body leaves both base word readings beyond form F under these conditions:

    • its delimiter is one quoted identifier and the operator is <<;
    • bash reads the operator at top level, and it is the only << on one physical line;
    • after the last ;/&&/|| and past reserved words, plain assignments, timeout N and output redirections, the command is an F operator line (a Python or Node program on stdin); a file a cat writes keeps both readings, whatever its name or exactly cat > FILE.py|.js|.mjs|.cjs;
    • a terminator line follows.

    The CODE rules read interpreter bodies as code. The whole-text, keyring-code and after-source checks read the whole text, and TAIL-READ reads every text after a terminator.

  • L3. SHELL-LITERAL reads a literal as bash does: name( opens no subshell. A method's only argument stays a standalone literal whatever the method's name. RESERVED stays off inside literal readings.

  • Pointer presence check. printenv POINTER… >/dev/null (standard output's last redirection on /dev/null) passes for inventory pointers only. [[ -v NAME ]] and test -n "${NAME+x}" already passed, and the environment_dump hint now names them.

  • Help and version. env or printenv with --help or --version as the only argument passes. Measured on the host's uutils 0.10.0: a synthetic marker variable is absent from all eight outputs and present in bare env's.

  • RESERVED (K4 tier k). A segment that begins with !, {, if, then, elif, else, while, until, do or coproc is read again, with each word-touching ( read as a blank as L3 does, as the command after them. This only refuses. The shapes and the measurement on main are in the api-actions lane's private record readers/guard-fp-20261012/FINDING-reserved-word-segments.md.

Replay (the landing condition). Every Bash call in this host's Claude Code transcripts, subagents included, timestamped 2026-10-11T00:00Z to 2026-10-13T00:00Z: 55,469 calls, 54,207 distinct commands. Each went through check() of main's guard (ce9027c56a7006317f52c6cfc2bcb9b267a4f37050269ccdbd001d1371f7d83a, at 6934527e) and of this PR's guard at this head (097c724fa6921a78274c654a41b04b6c0cce8f0001a667513447046e76fd0cb2, the bytes pinned in adoption/hooks/claude/SHA256SUMS). Earlier rounds are history: fix round 1's guard at b7fc4819 (8def443e5bf3d873a201cfc1994c228f825b82baabb87e6f3917d1fb33328ebf) gave 188 refusals, before condition (6); fix round 2's at 59251ed6 gave the same 197 as this head.

Class Main This PR
environment_dump 262 100
dotenv_read 24 24
interpreter_environment_unclassified 17 18
secret_name_search 14 14
environment_dump_after_source 13 13
credential_run_usage 5 5
service_manager_environment 5 5
credential_store_path 4 4
process_environment 4 4
secret_variable_reference 3 2
six other classes (one or two each) 8 8
All refusals 359 197
  • Newly refused: 0. No command that main allowed is refused by this PR, so none needs a true-positive classification.
  • Newly allowed: 162.
    • 152 here-document bodies: Python or JavaScript set(...), env, export and set read as shell commands.
    • 9 env/printenv --help or --version calls.
    • One Python string literal that names the key runner's usage and runs nothing.
  • One reason change, environment_dump → interpreter_environment_unclassified: a body now read as code.
  • The here-document set() class: main refused 232 commands for a word in a here-document body (a command main allows once its bodies are emptied); 152 pass here. The first version passed 218: the two exemptions withdrawn in fix round 1 account for 57 (49 files a cat writes, 8 data-method literals), and condition (6) of fix round 2 for 9 more (a construct that could rebind the interpreter or timeout, or an interpreter named by a path). The pointer case: the window's only pointer checks name the pointer through a loop variable (printenv $v), which still refuses because the guard cannot tell what $v names. The literal form passes and is tested for every inventory pointer.
  • The 100 remaining environment_dump refusals:
    • 15 env piped into a filter;
    • 79 here-documents outside L2 and L3: 49 files a cat writes, 8 data-method literals, 9 under condition (6), and 13 other shapes (interpreters named by a variable, unquoted delimiters, pipe filters outside F's list, written shell or prose files, literal shapes);
    • 2 loop pointer checks;
    • 1 printenv of a non-pointer;
    • 1 env -i with assignments only;
    • 2 inline programs that print the environment.
  • Replay record: rerun after each fix round against the merge-base guard (sha256 ce9027c56a7006317f52c6cfc2bcb9b267a4f37050269ccdbd001d1371f7d83a, unchanged at 703d588c) and this head's guard (sha256 097c724fa6921a78274c654a41b04b6c0cce8f0001a667513447046e76fd0cb2, the bytes pinned in adoption/hooks/claude/SHA256SUMS; fix round 1's was 8def443e5bf3d873a201cfc1994c228f825b82baabb87e6f3917d1fb33328ebf). The extractor, replayer, comparer, the corpus, both guards' bytes and every verdict file, with a SHA256SUMS, are in the api-actions lane's private records (readers/guard-fp-20261012/replay/). The corpus holds command text, so it is not published.

Pointers in non-interactive shells (the ruling asked for this).

  • Measured: the host's ~/.bashrc exports the pointers after its interactive guard. A Claude Code 2.1.296 Bash tool shell is neither interactive nor a login shell ($- = hmtBc), so the pointers are unset there. Bash reads only BASH_ENV for such a shell (bash(1), INVOCATION).
  • Chosen mechanism: Claude Code's user-scope env object in ~/.claude/settings.json. It sets variables for every session and its subprocesses (settings reference, env, cited below). The pointer block goes there as absolute paths; that configuration change is the command center's, after landing, from this PR's reviewed diff (its ruling of 19:35Z). Pointers are paths, never values, and the profile installer's env handling is for MCP servers only. The startup files stay the operator's.
  • Alternative not chosen: a SessionStart hook writing CLAUDE_ENV_FILE (hooks, "Persist environment variables") also works. It would add an installed hook for values that never change on a host.

Follow-ups (not in this change):

  • The 14 other here-document shapes that still refuse (13 environment_dump, 1 now interpreter_environment_unclassified) are recorded in the docs subsection; the command center ruled them follow-ups (2026-10-12, 19:35Z). An interpreter named by a variable ("$PY" - <<'PY') is the largest, and admitting it needs a literal-assignment reading.
  • env | grep … presence checks still refuse. The guard does not judge filters.
  • A file a cat writes (49 of the replay's refusals) and a data-method literal (8) could pass only where the guard proves no later command of the request runs the body, or that the receiver is the inert built-in. That proof is a new reading of its own, not part of this change.
  • From Astra's first read: the profile installer copies each destination in place, with no atomic replacement or rollback (install_claude_profile.py:146; predates this PR). Inherited hostname labels remain in the dispositions catalog and the evidence manifest (none in the patch or this body).
  • After landing: reinstall the user-scope copy with tools/adoption/install_claude_profile.py --only guard. The settings env entries for the non-interactive pointers are a Claude Code configuration change that the command center makes after landing, from this PR's reviewed diff.

Corrections (fix round 1, b7fc4819). Astra's full read of d2fcf5b5 returned CHANGES_REQUESTED with two P1 regressions against the merge base, nine reproduced allow/block changes in all (the read's verdict file holds them; the fixtures are the regression rows WRITTEN_AND_METHOD_REFUSED in tests/test_secret_path_guard.py):

  • P1-1: a file that a cat writes was masked by its name alone, and a later command of the same request could run its body. The written-file exemption is withdrawn, together with the residual the first version recorded for it.
  • P1-2: a literal that was a method's only argument was exempt by the method's name, although an attribute can hold any callable (CPython v3.13.16 Doc/library/types.rst:438, :461-462, SimpleNamespace). The data-method exemption is withdrawn.
  • The read's nine fixtures are regression rows (WRITTEN_AND_METHOD_REFUSED). They fail 9 times on d2fcf5b5's guard and pass on main's and here.
  • Withdrawing the written-file exemption made one replayed command refuse anew through RESERVED: Python's if set(...) in a written body read as shell. RESERVED now reads segments with L3's name( rule (k4_reserved_segments). The replay has no newly refused command.
  • P3s taken:
    • A test kills the literal-mode RESERVED mutant.
    • The redundant masked-body code-unit loop is removed (every masked body is an interpreter region the CODE rules already read).
    • The guard-hook record's line numbers are pinned to 958a6d1a.
    • The replay record now holds both guards' bytes.
    • The three Claude Code pages are re-captured with the same sha256, and the installed bash 5.3.9 manual is captured (readers/guard-fp-20261012/upstream/).

Corrections (fix round 2, 59251ed6). Astra's delta read of b7fc4819 returned CHANGES_REQUESTED: 1 P1, 1 P2 and 5 P3s (the read's verdict file holds the shapes; the fixtures are the first five REBOUND_DISPATCHER_REFUSED rows).

  • P1: L2 recognized the interpreter or timeout by its spelling, but the request itself could rebind that name before the body runs. An L2 body is now admitted only when its dispatcher is established (condition (6) in k4_code_bodies):
    • The interpreter must be named without a slash.
    • Nothing in the request outside the bodies may be able to rebind it or timeout. That rules out:
      • a function definition (name () or function);
      • alias, hash, enable, BASH_CMDS, BASH_ALIASES or PATH;
      • read, printf, mapfile, readarray or getopts;
      • declare, typeset, local, export or readonly with a name built from an expansion or -n;
      • eval, source, . or trap;
      • a command named by an expansion.
    • Any of these withdraws every L2 body, so each body keeps both base readings.
    • Source: bash(1) 5.3.9, "COMMAND EXECUTION", lines 2304-2316 of the installed-manual capture: a slash-free name is a function, then a builtin, then a hash-table or PATH entry. Measured on bash 5.3.9: a quoted or escaped function name is refused as "not a valid identifier", so a definition spells the name literally.
  • Regression rows: REBOUND_DISPATCHER_REFUSED holds 19 rows: the read's five plus the other routes above. Each fails on b7fc4819's guard (19 failing subtests), and main's guard refuses each one.
    • REBOUND_DISPATCHER_CONTROLS keeps five clean or literal cases allowed: a literal export, a quoted (), and hash -p to the real interpreter with a clean body.
  • Boundary: form F (L1, main's behaviour) still accepts an interpreter path. F is the whole command, so the request has no room to rebind before it. A file changed by an earlier Bash call is outside a per-command reading, as it was on main.
  • Replay (readers/guard-fp-20261012/replay/round2/, SHA256SUMS):
    • against main: 359 refusals before, 197 after; no command is newly refused, and none crosses the work budget;
    • against b7fc4819's 188: 9 recoveries are refused again, each holding one of the constructs above (1 function shape, 3 commands named by an expansion, 3 interpreter paths, 2 PATH).
    • A first version that withdrew L2 on any assignment builtin re-refused 23. A literal export NAME=value and a quoted () cannot rebind, so they no longer withdraw.
  • P2: the threat-model row no longer says a cat-written file is exempt. Every body a cat writes keeps both readings, as the docs subsection and the tests say.
  • P3s:
    • Taken: the CPython line locators (types.rst:438, :461-462; namespaceobject.c:77, :285, read at cbc944f4), and the fixture-source pointer above.
    • Follow-ups: the atomic install (already listed), inherited hostnames (already listed), and the absent private replay inputs. The reader's packet can carry the public captures, which are in readers/guard-fp-20261012/upstream/.
    • Not in this PR's files: tests/test_active_model_currency.py:947 assumes the unclipped notice wording.
  • Also in this round: a test comment that held a prefix of a private verdict digest now cites the verdict by name (the command center's ruling of 2026-10-12, 22:44Z).

Corrections (fix round 3, f91c0ac7). Astra's delta read of 59251ed6 (the read's verdict, a private record cited by name) found one new P1 and one P2. The command center's adjudication of 2026-10-13 (02:09Z) reopened the PR for one commit on those two findings; the P3s are Follow-ups.

  • P1: condition (6) could misread quoted text as shell syntax and so skip a later command.
    • The fix: operators are read from their unquoted source spelling, never from a quote-removed value, because quoting removes a metacharacter's special meaning (bash(1) 5.3.9, QUOTING, lines 483 and 500 of the installed-manual capture, sha256 dfb8e8e304601dd9de7098dba5b07fdbb08eb8c3b8093625bbd11474c873fc07). A command separator also ends any pending redirection target.
    • Regression: QUOTED_OPERATOR_REFUSED, the read's two cases first, run through check() and through the real hook.
      • The hook test fails 4 times on 59251ed6's guard and passes on main's and here.
      • The check() test fails 6 times there: the 4 rows and two new controls with quoted metacharacters, which the same fix keeps allowed.
  • P2: the replay section above and docs/secret-storage.md now bind their numbers to this head's guard digest, and name the earlier rounds' numbers as history.
    • The repair class against main's guard: 43 failures and one error in 9 tests, after 37 in round 1 and 41 in round 2.
    • The whole module against main's guard: 46 failures and 15 errors in 99 tests.
  • Replay (readers/guard-fp-20261012/replay/round3/, SHA256SUMS): 359 refusals on main, 197 here, none newly refused, and no change from 59251ed6.
  • Follow-ups from the read:
    • the OSV oauthlib expiry (the command center's incident);
    • the absent private replay inputs and source mirrors;
    • the in-place install copy;
    • inherited hostnames;
    • the clipped notice assertion in tests/test_active_model_currency.py.
    • Its P3 on a quoted () is fixed by the same change: that control is now allowed.

SOTA sources

  • GNU Bash Reference Manual: "Here Documents" (a quoted delimiter suppresses expansion in the body), "Redirections" (left-to-right; a here-document touches descriptor 0), "Reserved Words", "Compound Commands", "Shell Functions" and "Definitions" (( is a metacharacter). Also bash(1) "INVOCATION" (BASH_ENV for non-interactive shells), read from the host's installed manual on 2026-10-12.
  • GNU coreutils manual, "Common options" (--help and --version print and exit). Also the host's uutils coreutils 0.10.0 env/printenv, measured on 2026-10-12T18:52Z with a synthetic marker variable (help_probe.py in the replay record).
  • Claude Code settings reference, env: https://code.claude.com/docs/en/settings-reference.md, read 2026-10-12T19:05Z (sha256 4bec5505c7f33da3284848047b7e0c90148d777daf90b4cfda9da0a64878de08).
  • Claude Code hooks, "Persist environment variables": https://code.claude.com/docs/en/hooks.md, read 2026-10-12T19:05Z (sha256 31754d5adce5052af35acf85dc2ed207bb082b7dbf87f25218047b10a024a073).
  • Claude Code environment variables, CLAUDE_ENV_FILE: https://code.claude.com/docs/en/env-vars.md, read 2026-10-12T19:05Z (sha256 8097c27c0a5dd4b3428a61a9d3a79eb33b57c8eb30a3e5422d471d64c98f515f).
  • CPython v3.13.16 (cbc944f4), Doc/library/types.rst:438 (SimpleNamespace), :461-462 (its attributes are set from the arguments), and Objects/namespaceobject.c:77 and :285 (the arguments update the instance dictionary; attribute lookup is generic): an attribute can hold any callable, so a method name proves nothing about its receiver (the ground for withdrawing the data-method exemption).
  • Bash 5.3.9 (Ubuntu bash 5.3-2ubuntu1), bash(1) INVOCATION (BASH_ENV), RESERVED WORDS and REDIRECTION, captured from the installed manual in the lane's readers/guard-fp-20261012/upstream/ (sha256 dfb8e8e304601dd9de7098dba5b07fdbb08eb8c3b8093625bbd11474c873fc07).

Evidence-class table

Claim Evidence class Command / receipt
Refusals fall 359 → 188 (environment_dump 262 → 91) and no command is newly refused, over 54,207 distinct recorded commands local_integration replay.py and compare.py on main's guard and this PR's; private replay record with SHA256SUMS
The new tests fail on main's guard local_integration GuardFalsePositiveRepairTests alone, with main's secret_path_guard.py swapped in: FAILED (failures=37, errors=1); the whole module against main's guard: 40 failures and 15 errors. The round-1 regression rows fail 9 times on d2fcf5b5's guard (the command center's tests-only check, 2026-10-12): the other tests also exercise the new helpers and rows
A full environment dump after a reserved word passes main's guard local_integration battery on main's guard (private finding record)
env/printenv --help and --version print no variable native_proven help_probe.py on uutils 0.10.0: marker absent from 8 of 8 outputs, present in bare env
Claude Code Bash tool shells do not see ~/.bashrc pointers native_proven $- = hmtBc, shopt login_shell off, the pointer unset in the tool shell (2026-10-12)

Local commands run

$ python3 -m unittest tests.test_secret_path_guard
Ran 97 tests ...
FAILED (failures=1, skipped=2)   # test_host_profile_copy_is_verbatim, local only and skipped in CI: the installed user copy is
#   main's until the post-landing reinstall
$ python3 -m unittest tests.test_secret_path_guard.GuardFalsePositiveRepairTests   # this class only, with main's guard swapped in
FAILED (failures=37, errors=1)   # the whole module against main's guard: 40 failures, 15 errors
$ python3 -m unittest <the 37 test modules that name the guard, secret-storage, the dispositions, the hook pins or the profile installer> tests.test_upstream_surface_watch tests.test_evidence_manifest tests.test_validate
Ran 2863 tests in 922.784s
FAILED (failures=1, skipped=157)   # the same local-only test_host_profile_copy_is_verbatim
$ python3 scripts/validate.py
Integrity and scope checks only; no live provider or GPU execution.   (rc 0)
$ python3 scripts/evidence_manifest.py --check
{"files": 11800, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found   (rc 0)
$ git merge-tree --write-tree origin/main HEAD   # main 703d588c, after the fix commit
rc 0
# Fix round 2 (59251ed6):
$ fail_before.py <worktree> <b7fc4819's guard> GuardFalsePositiveRepairTests.test_a_rebindable_dispatcher_keeps_the_base_readings
subtest failures: 19   # with main's guard: 4, the clean controls only; with this head's: 0
$ python3 -m unittest tests.test_secret_path_guard tests.test_upstream_surface_watch tests.test_validate tests.test_evidence_manifest tests.test_install_claude_profile
Ran 428 tests in 105.586s
FAILED (failures=1, skipped=9)   # the same local-only test_host_profile_copy_is_verbatim
$ replay.py <this head's guard> cmds.jsonl; compare.py against main's and b7fc4819's verdicts
TOTAL blocks 359 -> 197, newly blocked 0 (against main); 188 -> 197, 9 None -> environment_dump (against b7fc4819)
$ python3 scripts/validate.py; python3 scripts/evidence_manifest.py --check
(rc 0); {"files": 11800, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found
$ git merge-tree --write-tree origin/main HEAD   # main c63c83a8, after the round-2 commit
rc 0
# Fix round 3 (f91c0ac7):
$ fail_before.py <worktree> <59251ed6's guard> GuardFalsePositiveRepairTests.test_a_quoted_operator_hides_no_command_from_the_hook
subtest failures: 4   # main's guard: 0; this head's: 0
$ fail_before.py <worktree> <59251ed6's guard> GuardFalsePositiveRepairTests.test_a_rebindable_dispatcher_keeps_the_base_readings
subtest failures: 6   # this head's: 0
$ python3 -m unittest tests.test_secret_path_guard tests.test_upstream_surface_watch tests.test_validate tests.test_evidence_manifest tests.test_install_claude_profile
Ran 429 tests in 96.472s
FAILED (failures=1, skipped=9)   # the same local-only test_host_profile_copy_is_verbatim
$ replay.py <this head's guard> cmds.jsonl; compare.py against main's and 59251ed6's verdicts
TOTAL blocks 359 -> 197, newly blocked 0 (against main); 197 -> 197, no transition (against 59251ed6)
$ python3 scripts/validate.py; python3 scripts/evidence_manifest.py --check
(rc 0); {"files": 11800, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found
$ git merge-tree --write-tree origin/main HEAD   # main c63c83a8, after the round-3 commit
rc 0

Decision record

The command center's ruling of 2026-10-12 (17:19Z) and its approval of 18:34Z are recorded in docs/secret-storage.md, "False-positive repair and reserved words (2026-10-12)", with the replay, the boundaries of each change and the non-interactive pointer mechanism.

Host evidence

Not applicable: no receipt changes.

Checklist

  • New/changed GitHub Actions are pinned to a full commit SHA with a
    version comment (no floating tags). (none changed)
  • New/changed workflows declare top-level permissions: {} and grant each job
    only what it needs (contents: read, or an explicitly justified addition). (none changed)
  • No secrets are printed, logged or committed; no new required secret was
    added without a documented owner.
  • No new paid hosting, subscription or billing surface was introduced.
  • Peer-owned untracked files and worktrees were preserved (not deleted,
    moved or overwritten).

🤖 Generated with Claude Code

… words

The command center's ruling of 2026-10-12: keep the secret-path guard and
remove its false positives. About nine in ten environment_dump refusals
of the last two days were Python or JavaScript code in a here-document
read as shell lines (a `set(...)` call became the `set` dump), and
printenv with any argument was a dump.

- L2 masks the body of an interpreter here-document beyond form F
  (behind a prefix, before trailing text, behind reserved words, an
  assignment, timeout or an output redirection) and of a Python or
  JavaScript file a `cat` writes, under the same exactness rules;
  the CODE rules still read interpreter bodies, the whole-text,
  keyring-code and after-source checks still read the whole text.
- L3 reads literal text as bash does before SHELL-LITERAL calls it a
  command: a `(` touching a word character opens no subshell, and a
  data method's only argument is a key or an affix.
- printenv of inventory pointers with standard output on /dev/null is
  a presence check; env and printenv --help/--version print no
  variable.
- RESERVED (K4 tier k) reads the command after leading bash reserved
  words, which no segment rule read before (private finding file).

A replay of 54,207 distinct Bash commands from 2026-10-11 and
2026-10-12 through main's guard and this one: refusals 359 -> 131,
environment_dump 262 -> 34, no command newly refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Oct 12, 2026
The new subsection said the operator adds the pointer entries to the
user-scope settings `env` object and that agents edit no settings. The
command center ruled at 19:35Z that this is a Claude Code configuration
change it makes after landing, from the reviewed diff. The sentence now
says so, and that the startup files stay the operator's. Five
dispositions locators into docs/secret-storage.md move down two lines;
both files are re-registered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins marked this pull request as ready for review October 12, 2026 20:16
Astra's full read of d2fcf5b (CHANGES_REQUESTED, 2 P1, 6 P3) reproduced
two regressions against the merge base 6934527, nine allow/block
changes in all; her verdict file holds the shapes.

- P1-1: a file that a `cat` writes was masked by its name alone, and a
  later command of the same request could run its body. The
  written-file exemption (k4_write_header) is withdrawn, with the
  residual the first version recorded for it.
- P1-2: a literal that was a method's only argument was exempt by the
  method's name, although an attribute can hold any callable (CPython
  v3.13.16 Doc/library/types.rst, SimpleNamespace). The data-method
  exemption is withdrawn.
- Her nine fixtures are regression rows (WRITTEN_AND_METHOD_REFUSED):
  9 failures on d2fcf5b's guard, passing on main's and here.
- Withdrawing the written-file exemption made one replayed command
  refuse anew through RESERVED (Python's `if set(...)` read as shell).
  RESERVED now reads segments with L3's `name(` rule
  (k4_reserved_segments), with rows for the pass and for a real dump
  beside a `name(`.
- P3s: a test kills the literal-mode RESERVED mutant; the redundant
  masked-body code-unit loop goes (every masked body is an interpreter
  region the CODE rules read); the guard-hook record's line numbers are
  pinned to 958a6d1.

Replay rerun over the same 54,207 commands against the merge-base guard:
refusals 359 -> 188, environment_dump 262 -> 91, none newly refused.
The guard pin, five dispositions locators and the manifest are
re-derived.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round 2 of #1048, after Astra's delta read at b7fc481 and the command center's tests-only delta (PASS_WITH_P3).

- P1: L2 recognized the interpreter or timeout by its spelling, but the same request could rebind that name
  (a function, alias, hash, enable, BASH_CMDS, BASH_ALIASES, PATH, a run-time variable name, eval, source, `.`,
  trap, a command named by an expansion) or name the interpreter by a path. k4_code_bodies now admits L2 bodies
  only when the interpreter is slash-free and nothing outside the bodies can rebind it or timeout (bash(1) 5.3.9,
  COMMAND EXECUTION). REBOUND_DISPATCHER_REFUSED (19 rows, the read's five first) fails 19 times on b7fc481's
  guard and is refused by main's; five controls stay allowed.
- The replay: main 359 refusals, this head 197, none newly refused; 9 of b7fc481's recoveries are refused again,
  each holding one of those constructs. K4_L2_ASSIGNMENT is anchored and possessive, so it stays linear under
  the no-backtracking test.
- P2: the threat-model row no longer says a cat-written file is exempt.
- A test comment cites a private verdict by name instead of a digest prefix (the ruling of 2026-10-12, 22:44Z).
- The guard pin, the evidence manifest and five catalog locators follow the new bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…his head's guard

Fix round 3 of #1048, on findings 1-2 of Astra's delta read at 59251ed (the command center's adjudication of
2026-10-13).

- Finding 1: condition (6) could misread quoted text as shell syntax and skip a later command. It now reads each
  operator from its unquoted source spelling (bash(1) 5.3.9, QUOTING), and a command separator ends any pending
  redirection target. QUOTED_OPERATOR_REFUSED runs through check() and the real hook; the hook test fails 4
  times on 59251ed's guard and passes on main's and here.
- Finding 2: docs/secret-storage.md binds the replay to this head's guard digest and the class counts to this
  head's tests, and labels the earlier rounds' numbers as history.
- The replay is unchanged from 59251ed: main 359 refusals, this head 197, none newly refused.
- The guard pin, the evidence manifest and five catalog locators follow the new bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins marked this pull request as draft October 13, 2026 03:36
seathatflowsinourveins added a commit that referenced this pull request Oct 13, 2026
…stody mode (#1057)

* Credential inventory: github-review-app key file and a --from-file custody mode

The command center's request of 2026-10-12 (22:06Z): the owner is
creating the review GitHub App, and its .pem private key needs custody
that no agent reads.

- adoption/credential-inventory.json: github-review-app, class
  github_app_private_key (new in scripts/credential_status.py), store a
  private_file at <store>/github-review-app.pem, no variable in the
  file, GITHUB_REVIEW_APP_ID (the App's ID, not secret) as an optional
  public variable kept beside its future loader, pointer
  GITHUB_REVIEW_APP_KEY_PATH, no loader yet.
- tools/credentials/set_credential.py --from-file (separate strict
  load_file_entry; the hidden prompt and --from-env still refuse the
  entry):
  - in the owner's terminal it asks for the downloaded file's path at a
    visible prompt;
  - it opens the file without following a symlink and refuses anything
    but a regular file of 1 byte to 64 KiB;
  - it copies the file unread and unprinted into a new 0600 temporary
    file through the checked store handle;
  - it checks that copy with `openssl pkey -noout -passin pass:` on
    standard input (exit status only);
  - only then does it give the copy its final name (replace needs the
    hidden word) and remove the downloaded file, if the typed path still
    names the copied inode.
  A failed check removes the copy and keeps the original.
- tools/credentials/open_credential_terminal.sh --from-file: the
  window's session runs the --from-file mode; the path is typed there,
  never written into the script.
- scripts/hooks/secret_path_guard.py: the new pointer joins
  POINTER_VARIABLE (a reader, copy or key tool on it is
  credential_file_read). Literal store paths were already refused.
- docs/secret-storage.md: the table row, the terminal usage and the
  custody paragraph, and the pointer line.
- Tests use RSA keys generated per test with openssl genpkey, never
  embedded:
  - custody: success, a non-key file, a passphrase-protected key, a
    symlink, a missing, empty or oversized source, replace, and no
    openssl;
  - the entry refused by the other modes, the launcher's dry run, the
    inventory row, and guard rows for the pointer.
  With main's files they give 5 failures and 13 errors.

The guard pin, five dispositions locators and the manifest are
re-derived.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Refuse a linked or foreign key source, name no path on failure, and record the custody reads

Fix round 1 of #1057, after Astra's first read at d9a7199 (findings 1-3) and the command center's tests-only check.

- Finding 1: --from-file refuses a source the operator does not own or that has another hard link, before anything
  moves, and removes the source only if it still has one link. Mode bits stay unchecked (WSL reports invented ones);
  the one link is removed once the stored copy passes its check.
- Finding 2: a failed file operation never shows its text or a path. Before the key is stored it is a refusal with
  fixed text and nothing moves; after it, the tool reports the key stored and the source cleanup incomplete (exit 3).
  The CLI's file mode gains the value-free handler --from-env already has. run_from_file looks up input and stdout
  per call. Six FileCustodyTests regressions fail on d9a7199's set_credential.py and pass here.
- Finding 3: a dated advisory gate-read delta, appended to the index, records the four custody reads from the
  native derivation at d9a7199 (348 reads against the #1016 baseline's 344); earlier deltas are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Refuse interpreter lookups of the App key pointer and close the custody link race

Fix round 2 of #1057, after Astra's delta read at 60fbe1c (two P1s and P3s; the command center's ruling of 05:31Z).

- Finding 1: interpreter code that looks the review App's key-file pointer up from its environment (os.environ,
  os.getenv, process.env, ENV) is refused as credential_file_read, as a whole-text rule after B(T)'s. stat, ls and
  test -f on the pointer stay allowed. Rows join BLOCKED and ALLOWED; a hook-protocol test runs the read's three
  forms. All fail on 60fbe1c's guard and pass here.
- Finding 2: after removing the source, the still-open descriptor must show no link left; otherwise custody reports
  the source cleanup incomplete (exit 3) and removes no unknown name.
- P3s: an interrupt after the key is stored reports that state; the stored key given as its own source changes
  nothing; the custody steps cite CPython v3.13.16 (os.rst replace, fsync, lstat, st_nlink).
- The guard pin, the evidence manifest and five catalog locators follow the new bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Drop the interpreter-reader guard rule from this change; keep the key-file custody

Scope reduction of #1057 (the command center's ruling of 2026-10-13, 06:24Z, after Astra's delta read at 74cd74c).

- The interpreter-reader protection family leaves this change: scripts/hooks/secret_path_guard.py and
  tests/test_secret_path_guard.py return to their 60fbe1c bytes, and the docs drop their sentences on it. Main's guard
  is unchanged for that family, so nothing is weaker than main; the family moves to the guard's owner under route B
  (native sandbox read denial, cc-native-practice).
- Kept: the --from-file custody mode with finding 2's post-removal link check and the round-2 custody P3s, the
  github-review-app inventory entry, the pointer row and their tests.
- The guard pin returns to round 1's value; the evidence manifest and five catalog locators follow the new bytes.

Net scope (CC adjudication df663230..., the #1048 precedent): the interpreter-reader refusal added by the third
commit is removed again by the fourth, so main's text guard is unchanged for that family, which moves to the native
sandbox route. What lands is the github-review-app key file, the --from-file custody mode, the linked or foreign
source refusals and the custody records.
Follow-ups: the 4 in Astra's fix3 delta at f3619df (verdict sha256 b9f6db68...); after landing, the CC reinstalls
the host guard copy (tools/adoption/install_claude_profile.py --only guard).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant