Skip to content

Guard leak-path fix: read timeout's -- and post-duration options as uutils coreutils does - #685

Merged
seathatflowsinourveins merged 10 commits into
mainfrom
foundation/guard-timeout-separator-20261004
Oct 4, 2026
Merged

seathatflowsinourveins merged 10 commits into
mainfrom
foundation/guard-timeout-separator-20261004

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Scope

  • What this PR changes, in one or two sentences: a defensive leak-path fix for the command guard (scripts/hooks/secret_path_guard.py).
    • The guard took the word right after timeout's duration for the command. uutils timeout, the coreutils of Ubuntu 26.04, still reads an end-of-options -- and options at that position and runs the command after them, so the guard never examined that command.
    • Both word readings now step over that separator and those options.
    • The current reading also reads the launcher grammar the audit found open: long-option prefixes, env's value options and clusters, and the uutils multi-call binary.
  • Base commit: f77a35eb2bf30bc4bf6f3b4ee51bc9ce5397b4c5
  • Lane: lane:foundation. Re-registering hashes in manifests/evidence.json does not make it shared (docs/lanes.md, "Labels and PRs").
  • Owned paths touched:
    • scripts/hooks/secret_path_guard.py and tests/test_secret_path_guard.py;
    • docs/decisions/2026-09-24-secret-storage.md (append-only dated addendum);
    • adoption/hooks/claude/SHA256SUMS (the guard's install pin, without which the profile installer refuses the changed guard);
    • manifests/evidence.json (hash re-registration, in the last commit).

SOTA sources

All sources were read on 2026-10-04 (UTC).

uutils coreutils (https://github.com/uutils/coreutils), at tags 0.8.0 (commit c4093734e2eb), 0.10.0 (28b6856d7b21) and 0.12.0 (dc1efd89948a):

GNU coreutils (mirror https://github.com/coreutils/coreutils of git.savannah.gnu.org/git/coreutils.git), at v9.7 (8e075ff8ee11) and v9.12 (c0f8514d9891):

  • src/timeout.c: getopt_long (argc, argv, "+fk:ps:v", long_options, ...) and its long_options.
  • src/env.c: shortopts "+a:C:iS:u:v0", and longopts with argv0, plus env0-from at v9.12.
  • src/nice.c, src/stdbuf.c and src/nohup.c.
  • The manual's source, doc/coreutils.texi at v9.12:
    • node "timeout invocation": synopsis timeout [option]… duration command [arg]…, and "Options must precede operands";
    • "Common options": "Most programs that accept long options recognize unambiguous abbreviations of those options".
  • The rendered page is https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html. www.gnu.org timed out from the working host on 2026-10-04, so the Texinfo source the page is built from is cited instead.

glibc manual (mirror https://github.com/bminor/glibc), at glibc-2.42:

Ubuntu packaging: the resolute file list of rust-coreutils 0.8.0-0ubuntu3, which ships /usr/bin/coreutils: https://packages.ubuntu.com/resolute/amd64/rust-coreutils/filelist

sudo-rs (https://github.com/trifectatechfoundation/sudo-rs), at v0.2.15: src/sudo/cli/mod.rs normalize_arguments matches long options exactly and ends options at --. Audited; no change needed.

uutils 0.8.0 release binary: coreutils-0.8.0-x86_64-unknown-linux-gnu.tar.gz, sha256 ea8f0a4f6815c1da…, equal to the digest the release page publishes. It was run for the measurement below.

Evidence-class table

Claim Evidence class Command / receipt
At f77a35eb the guard allows timeout 5 -- printenv APCA_API_SECRET_KEY, timeout 5 -- env, timeout 5s -- gdb -p 1, timeout --signal=TERM 5 -- printenv … and 25 more launcher forms that run a refused command local_integration, failing first Committed tests against the base guard: 137 failing subtests, exit 1 (see below)
This head refuses all 31 blocked rows and keeps the 15 negative controls (timeout 5 -- ls …) allowed local_integration Same tests with the head guard: exit 0
uutils timeout reads options and -- after the duration and takes long-option prefixes; GNU stops options at the duration source_review uutils timeout.rs:177 (0.8.0), :171 (0.10.0), :173 (0.12.0); GNU timeout.c "+fk:ps:v"; coreutils.texi "Options must precede operands"
uutils 0.8.0 runs the command in timeout 5 -- CMD, timeout -k 2 5 -- CMD, timeout 5 -s KILL CMD, timeout 5 -v CMD, env -a NAME CMD and env -f FILE CMD, where GNU 9.4 exits 127 or 125 local_integration (upstream release binary run locally with echo only; output quoted here; no receipt) Local run, 2026-10-04T07:22Z
Both implementations run --sig, --kill, --adj, --out, --un and env -vu NAME CMD; both exit 127 for timeout -- 5 -- CMD and timeout 5 -- -- CMD; an argv0 override does not choose the multi-call utility on Linux local_integration The same local run
On Ubuntu 26.04.1, uutils 0.8.0 runs timeout 5 -- CMD and GNU 9.7 exits 127 Cited from the coordinator's coreutils-workflow measurement, not reproduced here Task brief
No command the base guard refuses is allowed, and no documented command is newly refused local_integration (differential replay) See the replay summary below the table
The installer accepts the changed guard local_integration See the dry-run result below the table

Replay summary: 6,582 distinct commands were checked against the base guard.

  • Inputs: 1,500 test strings, 4,792 lines and whole blocks from 880 fenced shell blocks of tracked Markdown, and 290 launcher-matrix forms.
  • Results: 159 newly refused (the 29 new test rows and 130 matrix rows, every one starting printenv, env, gdb or cat .env), 0 loosened, 0 reason changes, 0 errors.

Installer dry run: install_claude_profile.py --home <empty scratch> --only guard --hook secret_path_guard.py --dry-run reported "would install … (sha256 c6b8cfdc073d…)" and wrote nothing (exit 0).

Local commands run

All runs used TMPDIR=<scratch> nice -n 19.

$ python3 -m unittest tests.test_secret_path_guard
Ran 85 tests  FAILED (failures=1, skipped=2)  exit 1
  the only failure is test_host_profile_copy_is_verbatim: this host's installed guard is still the
  base guard (sha256 7b1ec8fa...), to be reinstalled after merge (not done here, by design)
$ HOME=<empty scratch> python3 -m unittest tests.test_secret_path_guard
Ran 85 tests  OK (skipped=3)  exit 0
$ python3 -m unittest tests.test_command_position_oracle
Ran 4 tests  OK  exit 0
$ python3 -m unittest tests.test_install_claude_profile tests.test_adoption_status tests.test_adoption_docs_consistency \
    tests.test_freeze_snapshot tests.test_new_wsl_client_config tests.test_credential_status \
    tests.test_credential_boot_receipt tests.test_canary_proof
Ran 765 tests  OK (skipped=5)  exit 0
$ python3 scripts/validate.py
{"components": 69, "hashed_files": 9864, "profiles": 4, "receipts": 195, "status": "passed"}  exit 0
$ python3 scripts/evidence_manifest.py --check
{"files": 9864, "status": "passed"}  exit 0
$ python3 scripts/component_matrix.py --check ; python3 scripts/new_host_grand_list.py --check
exit 0 ; exit 0
$ git diff --check origin/main...HEAD
exit 0
$ python3 -m unittest    (the whole suite)
Ran 10246 tests  FAILED (failures=2, skipped=871)  exit 1
  test_host_profile_copy_is_verbatim: expected, see above
  test_windows_terminal_defaults ... test_the_installed_client_knows_no_notification_type_without_a_decision:
  this host's installed client knows 'auth_storage_failure'; the test fails identically on a pristine
  origin/main export (6af8e55b), so it is environmental and not caused by this PR
$ main moved to 6af8e55b (#679, #681): git merge-tree --write-tree HEAD origin/main
clean, exit 0; on the merged tree validate.py and evidence_manifest --check exit 0 (9,878 hashed files),
and the guard, its tests and the pin are unchanged by the merge
$ failing first: the committed tests against the base guard (git archive HEAD, guard file from f77a35eb)
FAILED (failures=137)  exit 1
  test_launcher_grammar_of_gnu_and_uutils_coreutils 29, test_blocked_commands 29,
  test_every_rule_applies_behind_rtk_proxy 58, test_every_rule_applies_to_a_keyring_exec 6,
  test_both_readings_take_the_command_after_timeouts_duration 6,
  test_the_current_reading_reads_gnu_and_uutils_env_and_the_multicall_binary 9
$ the same tests with the head guard
OK  exit 0

Wrapper audit

Launcher -- handling GNU vs uutils (source at tag) Verdict
timeout Before the duration -- ends options for both. After it, GNU runs -- (exit 127) and uutils skips it uutils reads options after the duration (clap trailing_var_arg); both take long-option prefixes Real. Fixed in both readings (separator, post-duration options) and, for prefixes, in the current reading
nice Ends options for both; already read Both take --adj; uutils' legacy -N rewrite matches GNU's Real (prefix). Fixed in the current reading
stdbuf Ends options for both; already read Both take --out, --o Real (prefix). Fixed in the current reading
nohup Ends options for both; already read Only --help/--version Not real: no value option
env Ends options for both; the walk steps over it and reads on (errs toward refusal) uutils -f/--file; -a/--argv0 in uutils and GNU ≥ 9.7; --env0-from in GNU 9.12; clusters (-vu NAME); prefixes Real. Fixed in the current reading; the prior reading keeps c26800f's table, so nothing it refused passes
coreutils (multi-call) n/a uutils only; /usr/bin/coreutils on Ubuntu 26.04 Real. coreutils UTIL is read as UTIL
sudo sudo-rs v0.2.15: -- ends options sudo-rs (Ubuntu's sudo, not uutils) matches long options exactly Not real for sudo-rs; C sudo's long-option prefixes were not audited
doas, command, builtin, exec, time, setsid, ionice, xargs, chronic, systemd launchers Already read Not part of coreutils, so no uutils grammar applies Out of this audit's scope; long-option prefixes of the getopt_long ones were not audited

Decision record

docs/decisions/2026-09-24-secret-storage.md, "Addendum 2026-10-04: launcher grammar of GNU and uutils coreutils". It covers the decision, sources, measurement, alternatives rejected, overturn conditions, open items and evidence class.

Residuals

  • env -S STRING followed by more words (env -S 'CMD …' more): the split string becomes the command line. This is a different class (a string-splitting launcher) and is not fixed here.
  • Long-option prefixes of the launchers outside coreutils (C sudo, xargs, time, ionice, the systemd launchers) were not audited.
  • GNU's single-binary coreutils --coreutils-prog=NAME, and other multi-call binaries (busybox, toybox), are not read.
  • examples/claude-native/workflows/child-usage.mjs (lane counting, not credential protection) reads timeout the GNU way, so on a uutils host it would not count the lane in timeout 5 -- LANE. It is outside this PR's paths, and its oracle passes on GNU hosts.
  • Deliberate over-read: timeout -- 5 -- CMD is refused, although both implementations run -- there and exit 127. It errs toward refusal and no documented command uses it.
  • The installed guard on each host must be reinstalled after merge (see below).

Host reinstall after merge (coordinator)

python3 tools/adoption/install_claude_profile.py --only guard --hook secret_path_guard.py
python3 -m unittest tests.test_secret_path_guard.SecretPathGuardTests.test_host_profile_copy_is_verbatim

Host evidence

Not applicable: no file under evidence/hosts/ 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: contents: read
    (or a narrower, explicitly justified addition). (None changed.)
  • No secrets are printed, logged or committed; no new required secret was
    added without a documented owner. Test strings use placeholder names only.
  • 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

Scout and others added 2 commits October 4, 2026 03:24
… uutils does

The command guard took the word right after timeout's duration for the
command. GNU timeout runs that word (a `--` there exits 127), but uutils
timeout, the coreutils of Ubuntu 26.04, still reads options and an
end-of-options `--` after the duration (clap trailing_var_arg,
timeout.rs:177 at 0.8.0, :171 at 0.10.0, :173 at 0.12.0). So
`timeout 5 -- CMD` and `timeout 5 -s KILL CMD` started a command the guard
never examined.

Both word readings now step over those options and the separator. The
current reading also takes unambiguous long-option prefixes of timeout,
nice, stdbuf and env (getopt_long; clap infer_long_args), env's value
options of both implementations (-a/--argv0, -f/--file, --env0-from) and
clusters a value letter ends, and the uutils multi-call binary
(`coreutils UTIL`). The prior reading keeps c26800f's env table, so no
command refused before passes.

Failing-first: 29 of the 31 new blocked rows failed at f77a35e; all pass
now. A replay of 6,582 commands against the base guard found 0 loosened,
0 reason changes and 0 documented commands newly refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
adoption/hooks/claude/SHA256SUMS pins the guard's new sha256, so the
profile installer and the client-configuration check accept the changed
guard. manifests/evidence.json carries the new hashes of the guard, its
tests, the secret-storage decision and SHA256SUMS. Registry edits are in
the branch's last commit (hot-file rule).

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 4, 2026
Scout and others added 5 commits October 4, 2026 10:47
…et guard

Preserve GNU hyphen-leading command paths in both timeout readings. Expand literal env split strings with trailing argv, and recognize both GNU single-binary dispatch forms. Restore harmless split-command controls and retain failing-first and scoped differential evidence.

Co-Authored-By: GPT-6.1 Sol (OpenAI Codex) via the packaged SDK worker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…le protocol, last commit)

Co-Authored-By: GPT-6.1 Sol (OpenAI Codex) via the packaged SDK worker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read GNU split-string argv and uutils preprocessing independently. Refuse
inferred split options whose payload uutils discards, follow literal split
prefixes after assignment operands, and consume generated split options
without recursively pre-expanding them. Preserve the raw env no-command
backstop and follow GNU coreutils dispatch in the independent reading.

Turn the invalid --split allow controls into refusals, retain the harmless
separate -vS control, and give the previously masked test a trailing
placeholder operand. Correct the decision's GNU/uutils grammar claims and
record both proven mistakes in the repository anti-pattern log.

Content files: scripts/hooks/secret_path_guard.py,
tests/test_secret_path_guard.py,
docs/decisions/2026-09-24-secret-storage.md, docs/harness-defaults.md.

Validation with CI=true and the requested TMPDIR:
- 95 regression subtests fail at a7888d3 and pass after this repair.
- python3 -m unittest tests.test_secret_path_guard: 89 tests, 3 skips, passed.
- python3 scripts/validate.py: passed.
- Regenerated replay: 3,257 commands; 232 newly refused against f77a35e,
  95 against a7888d3, zero loosened, reason changes, or errors against both.
- git diff --check: passed.

The requested temporary directory is absent; Python used its /tmp fallback.
Sources: GNU coreutils v9.12 src/env.c and uutils coreutils 0.10.0/0.12.0
src/uu/env/src/env.rs, with the supplied 0.8.0 source comparison.

Co-Authored-By: GPT-6.1 Sol (OpenAI Codex) via the packaged SDK worker

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Update adoption/hooks/claude/SHA256SUMS for the repaired guard and
re-register the guard, tests, decision, anti-pattern log and pin through
scripts/host_receipts.py register_file. Preserve all other registry rows.

Keep only adoption/hooks/claude/SHA256SUMS and manifests/evidence.json in
this final commit, after the content commit, per docs/lanes.md.

Guard SHA256:
9a12a0a0d036b3c6c84cbb52ec4149c47265f091c87da30168bb73c3dc757f5d

Validation: CI=true python3 scripts/validate.py passed with 69 components,
9,864 hashed files, 4 profiles and 195 receipts; git diff --check passed.
These are integrity and scope checks, with no live provider or GPU run.

Co-Authored-By: GPT-6.1 Sol (OpenAI Codex) via the packaged SDK worker

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ilt from main plus this PR's files

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Scout and others added 3 commits October 4, 2026 13:55
Round two allowed commands that main or round one refused by replacing GNU
env expansion and the historical command tail with a uutils reading. Keep
the raw, GNU-expanded and uutils-preprocessed candidates in a bounded work
list, and apply ENV_ARG_OPTIONS to every argv. Include inferred long-option
value widths. Before coreutils dispatch continues, queue any keyring command
found in its raw words so consumed option/script operands retain protection.

Add R1, R2, R3, R4, R5 and R5b under the existing prefix loops. Turn the
supplied oracle into a permanent comparison against immutable, hash-pinned
main and round-one fixtures, including existing rows and launcher variants.
Correct the guard comments, decision record and anti-pattern log.

Validation with TMPDIR=/tmp/guard685-r3 and CI=true:
- Four selected methods fail at ec382e2 with 92 assertions and 2 existing
  work-budget errors; the repaired methods pass.
- python3 -m unittest tests.test_secret_path_guard: 90 tests, 3 skips, passed.
- Supplied monotonicity_probe.py: all 18 working cases refused, no errors.
- Permanent baseline comparison: 5,149 existing rows and variants.
- Regenerated replay: 5,452 commands; 271 newly refused against f77a35e,
  95 against a7888d3, zero loosened, reason changes or errors against both.
- python3 scripts/validate.py and git diff --check: passed.

Sources: GNU coreutils v9.12 src/env.c and src/coreutils.c; uutils coreutils
0.10.0/0.12.0 src/uu/env/src/env.rs; the supplied Sonnet allow-path oracle;
this repository's f77a35e and a7888d3 guard snapshots.

Co-Authored-By: GPT-6.1 Sol (OpenAI Codex) via the packaged SDK worker

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Update adoption/hooks/claude/SHA256SUMS for the repaired guard and
re-register the changed guard, tests, decision, anti-pattern log, integrity
pin and four baseline/oracle fixture files through host_receipts.register_file.
Preserve all other manifest data.

Keep only adoption/hooks/claude/SHA256SUMS and manifests/evidence.json in
this final commit after the content commit, following docs/lanes.md.

Guard SHA256:
015b8c6e1ccec2df8232465cc9c0f0e40244d97ee0fa892c8a95faac816e4bf5

Validation: python3 scripts/validate.py passed with 69 components, 9,886
hashed files, 4 profiles and 195 receipts; git diff --check passed.
Integrity and scope evidence only; no live provider or GPU run.

Co-Authored-By: GPT-6.1 Sol (OpenAI Codex) via the packaged SDK worker

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

Copy link
Copy Markdown
Collaborator Author

Claude session native-agent-stack-5f: landing at head c87484e314d66b95e4f96fac4e8f5873a919baf5. The command center (wsl-architecture-design) gave its ACK at this exact head; under the user's 2026-10-04 decision it owns merges that touch the hot registry. The read-only cross-family review (GPT-6.1 Sol max, the packaged Codex SDK worker) returned FINDINGS at 385e3f6584d2; this head is reached from it through recorded carry edges (equal owned patch-ids, or a cross-family delta read returning ACCEPT at the edge's target), with its 3 P1 item(s) resolved in one repair round as recorded.

Observed main d323b53437e025be3d054b9b5e4d292fe396c75e. coordination/merge_tree_landing_check.py (sha256 a11649fcf2e5844c…) exit 0:

main d323b53437e025be3d054b9b5e4d292fe396c75e head c87484e314d66b95e4f96fac4e8f5873a919baf5 base df50444a4d58168c35829f3c6b1a663af9500783 merged-tree 8d56141ce957158c03a4a0bc5d349a65bb4aab83 merge-tree-exit 0
ok   1: clean three-way merge
ok   2: merged-vs-main paths 10, outside PR-owned 0 []
ok   3: main drift 11 paths, overlap with PR-owned inputs (registry excluded) []
ok   4: registry foreign rows equal True, order preserved True, PR-owned rows 9, unowned top-level keys differing []
ok   5: merged files[] sorted by path with no duplicates: True (9893 rows)
LANDABLE
condition 5: merged files[] sorted and unique (tree 8d56141ce957158c03a4a0bc5d349a65bb4aab83)

Required checks at this head: 8 pass . Unresolved review threads: 0.

@seathatflowsinourveins
seathatflowsinourveins merged commit 780bf5d into main Oct 4, 2026
26 checks passed
@seathatflowsinourveins

Copy link
Copy Markdown
Collaborator Author

Claude session native-agent-stack-5f: post-merge observation. Landed as 780bf5d0508760b2494995493d9246efd74d614d (parent d323b53437e025be3d054b9b5e4d292fe396c75e), tree 8d56141ce957158c03a4a0bc5d349a65bb4aab83. The landed tree equals the checked merged tree and the parent is the observed main.

@seathatflowsinourveins
seathatflowsinourveins deleted the foundation/guard-timeout-separator-20261004 branch October 4, 2026 18:35
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