Skip to content

Shell capture cleanup and generated-content policy guard#4298

Merged
Trecek merged 7 commits into
developfrom
shell-capture-hook-injects-prohibited-rm-cleanup-and-blocks/4291
Jul 20, 2026
Merged

Shell capture cleanup and generated-content policy guard#4298
Trecek merged 7 commits into
developfrom
shell-capture-hook-injects-prohibited-rm-cleanup-and-blocks/4291

Conversation

@Trecek

@Trecek Trecek commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

/audit-impl re-audited the Part A implementation of the shell-capture-hook rm -f → Python-side-sweep migration against the original plan and found the implementation faithful on 47 of 49 tracked requirements. This PR fixes the two remaining defects — replacing the wrong time reference in sweep_stale_captures and parametrizing the parity test over all four attack vectors — and lands the broader shell-capture-cleanup architecture and generated-content policy guard that those fixes sit within.

Individual Group Plans

Audit-implementation remediation

/audit-impl re-audited the Part A implementation of the shell-capture-hook rm -f → Python-side-sweep migration (commits 12e85051e, 29095a56b) against the original plan and found the implementation faithful on 47 of 49 tracked requirements. Two genuine defects remain and block merge:

  1. sweep_stale_captures (src/autoskillit/hooks/_capture_cleanup.py) computes staleness against the wrong time reference. It derives its "now" proxy from capture_dir_path.resolve(strict=True).stat().st_mtime — the capture directory's own mtime — instead of true wall-clock time (time.time() / datetime.now()). A directory's mtime only advances when an entry inside it is created, removed, or renamed; it is not a stand-in for "the current moment." This means a file created long before the directory's last entry-change can look "fresh" and be skipped, and files can go permanently unswept if the directory has had no new entries for a while — even though the sibling session_start_hook.py inline sweep block already implements the correct wall-clock semantics (_now = datetime.now(UTC); _c_age = _now.timestamp() - _cp.stat().st_mtime). Every existing test masks this because each one calls sweep_stale_captures(..., max_age_seconds=0) immediately after creating the capture directory, so dir_mtime ≈ wall_clock_now at call time and the bug never surfaces.

  2. test_capture_cleanup_containment_parity (tests/hooks/test_shell_capture_conformance.py) omits two of the four required attack vectors and is not parametrized. The original plan (Step 2g) requires this test to verify _is_safe_capture_file rejects the same attack vectors as the canonical resolve_contained_path (core/path_containment.py): symlinks, traversals, hardlinks, and world-writable files, via parametrized cases, so future drift between the two independently-maintained implementations is caught. The current test only exercises a valid file, a symlink, and a traversal-shaped filename — it never constructs a hardlink or a world-writable file, so the st_nlink > 1 and st_mode & 0o002 branches of _is_safe_capture_file are completely untested by the test whose stated purpose is to guard exactly those branches.

This plan fixes both defects with the minimum change needed to make the implementation match its own stated design intent. It does not touch the generated-content policy guard, live conformance probe strengthening, or codex_status verification architecture — those remain reserved for Part B (a separate task, out of scope here) per the original plan's Part A/B split.

Shell capture cleanup architecture

shell_capture_hook.py:113 injects rm -f -- "$__as_f" into every Codex shell command's generated harness. Codex CLI 0.144.6's exec-policy engine (a compiled-in rule introduced at rust-v0.144.5) forbids rm -f-style commands, causing universal command rejection. The inline shell deletion is architecturally anomalous — it is the only shell-embedded deletion in the entire package — and the "spill" branch (>12KB output) never cleaned up at all.

This part removes the shell-embedded deletion, replaces it with a Python-side stdlib-only cleanup module in the hook subprocess, adds a TTL sweep for orphaned captures, and updates the conformance tests and documentation to match the new behavior.

Part B will cover the generated-content policy guard, live conformance probe strengthening, and codex_status verification architecture — implement as a separate task.

Generated-content policy guard and conformance verification

This part addresses the recurring architectural gap: hook-generated artifacts (shell scripts, TOML config) are validated against models/snapshots, never against the installed Codex binary's actual enforcement layers. This is the third incarnation of this failure class (write_guard divergences, output_budget_guard false positives, shell_capture policy collision).

The solution introduces:

  1. A static architectural guard test asserting no hook-generated harness/template contains destructive shell verbs — a CI-native, no-network defense
  2. A strengthened live conformance probe that distinguishes policy rejection from transport failure and asserts on execution success (not substring presence)
  3. Documentation of the capture-artifact lifecycle (ownership, retention, cleanup timing)

Part A covered the rm -f removal, Python-side cleanup, and sweep architecture.

Implementation Plan

Plan files:

  • /home/talon/projects/autoskillit-runs/remediation-20260718-192507-037270/.autoskillit/temp/rectify/rectify_shell_capture_hook_rm_cleanup_and_generated_content_guard_2026-07-18_192507_part_a.md
  • /home/talon/projects/autoskillit-runs/remediation-20260718-192507-037270/.autoskillit/temp/make-plan/shell_capture_hook_rm_cleanup_remediation_plan_2026-07-20_041550.md
  • /home/talon/projects/autoskillit-runs/remediation-20260718-192507-037270/.autoskillit/temp/rectify/rectify_shell_capture_hook_rm_cleanup_and_generated_content_guard_2026-07-18_192507_part_b.md

Closes #4291

🤖 Generated with Claude Code via AutoSkillit

Token Usage Summary

Step Model count uncached output cache_read peak_ctx turns cache_write time
review_approach* sonnet 1 3.1k 8.4k 568.4k 85.2k 59 13.5k 4m 50s
dry_walkthrough* sonnet 3 82.3k 138.8k 10.9M 164.0k 224 523.4k 26m 4s
implement* MiniMax-M3 3 329.3k 47.3k 9.0M 0 315 0 27m 22s
assess* MiniMax-M3 1 79.8k 9.1k 1.5M 0 61 0 8m 53s
audit_impl* sonnet 3 142.0k 186.9k 32.2M 199.4k 410 603.6k 36m 32s
make_plan* sonnet 1 4 994 298.6k 150.8k 54 3.4k 8m 58s
prepare_pr* MiniMax-M3 1 56.0k 3.9k 183.8k 0 17 0 1m 53s
compose_pr* MiniMax-M3 1 39.7k 4.3k 180.2k 0 14 0 50s
review_pr* sonnet 1 1.3M 6.9k 1.3M 0 0 0 6m 19s
ci_conflict_fix* sonnet 1 22.4k 8.3k 2.2M 97.5k 57 71.8k 2m 25s
Total 2.1M 415.0k 58.3M 199.4k 1.2M 2h 4m

* Step used a non-Anthropic provider; caching behavior may differ.

Token Efficiency

Step LoC Changed cache_read/LoC cache_write/LoC output/LoC
review_approach 0
dry_walkthrough 0
implement 567 15834.9 0.0 83.4
assess 10 149708.8 0.0 906.8
audit_impl 0
make_plan 0
prepare_pr 0
compose_pr 0
review_pr 0
ci_conflict_fix 2289 954.4 31.4 3.6
Total 2866 20343.4 424.2 144.8

Model Usage Breakdown

Model steps uncached output cache_read cache_write time
sonnet 6 1.6M 350.4k 47.5M 1.2M 1h 25m
MiniMax-M3 4 504.8k 64.6k 10.8M 0 38m 59s

Trecek and others added 7 commits July 19, 2026 22:56
…ionStart sweep

Removes the inline 'rm -f -- \"\$__as_f\"' deletion embedded in every Codex
shell command's generated harness. Codex CLI 0.144.6's exec-policy engine
forbids rm -f-style commands, causing universal command rejection. Cleanup
now lives in a stdlib-only SessionStart TTL sweep via the new
_capture_cleanup module.

- Add src/autoskillit/hooks/_capture_cleanup.py with _is_safe_capture_file
  containment checks (symlink, hardlink, world-writable, traversal) and
  sweep_stale_captures deletion guarded by 1h age threshold
- Widen shell_<uid> uid from 8 to 16 hex chars (collisions under fleet
  concurrency over the now 1h retention window)
- Wire the sweep into session_start_hook.py alongside kitchen_state and
  pipeline_tracker sweeps (stdlib-only boundary preserved via inlined
  _CAPTURE_RE literal)
- Add timeout_seconds=5 to shell_capture_hook HookDef matching the
  ceiling used by open_kitchen_guard / ask_user_question_guard /
  mcp_health_advisor
- Add conformance tests: destructive-verb absence, small-output retention
  + sweep cleanup, symlink rejection, filename allowlist, containment
  parity vs core/path_containment.py, regex consistency between the two
  sweep sites
- Invert the existing 'assert not artifacts' assertion to assert the
  Python-side retention invariant

Fixes #4286 follow-up. Part A of the rectify plan only.

Co-Authored-By: Claude <noreply@anthropic.com>
…mtime

`sweep_stale_captures` previously derived its staleness threshold from
`capture_dir_path.resolve(strict=True).stat().st_mtime` — the capture
directory's own mtime, which only advances when an entry inside the
directory is created, removed, or renamed. This meant a file created
long before the directory's last entry-change could look "fresh" and be
skipped, and files could go permanently unswept if the directory had
no new entries for a while.

Replace the directory-mtime proxy with `time.time()` so staleness is
measured against real wall-clock time, matching the inline sweep block
in `session_start_hook.py` (`_now.timestamp() - _cp.stat().st_mtime`).
The strict-resolve existence/race check is retained; `capture_dir_resolved`
is dropped since it is no longer referenced.

Every existing test passes `max_age_seconds=0` immediately after creating
the capture directory, so `dir_mtime ≈ wall_clock_now` at call time and
the bug never surfaced. A new regression test in the next commit covers
the case where the directory's mtime is equally stale to its contents.
…k vectors

`test_capture_cleanup_containment_parity` previously exercised only a
valid file, a symlink, and a traversal-shaped filename — leaving the
`st_nlink > 1` and `st_mode & 0o002` branches of `_is_safe_capture_file`
completely untested by the test whose stated purpose is to guard
exactly those branches.

Convert the test to `@pytest.mark.parametrize` over five module-level
case-builders covering all four attack vectors from
`core.path_containment.resolve_contained_path` (symlinks, traversals,
hardlinks, world-writable files), plus the valid baseline. Hardlink
case follows the existing `pytest.skip("hardlink not supported...")`
precedent for filesystems where hardlinks are unsupported.

Also add `test_sweep_stale_captures_uses_wall_clock_not_directory_mtime`
which backdates both a capture file and its enclosing directory by 200s
and asserts the sweep deletes it under `max_age_seconds=100`. This
reproduces the staleness-threshold defect fixed in the previous commit
(directory-mtime proxy instead of wall-clock time) and would have
failed against the prior implementation.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@Trecek
Trecek force-pushed the shell-capture-hook-injects-prohibited-rm-cleanup-and-blocks/4291 branch from 2c18573 to d248752 Compare July 20, 2026 05:58
@Trecek
Trecek added this pull request to the merge queue Jul 20, 2026
Merged via the queue into develop with commit 3d7829d Jul 20, 2026
3 checks passed
@Trecek
Trecek deleted the shell-capture-hook-injects-prohibited-rm-cleanup-and-blocks/4291 branch July 20, 2026 06:13
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.

1 participant