Skip to content

fix(disk-hygiene): warn a resumed session that the clean skill's guard is gone - #10091

Open
kyle-sexton wants to merge 6 commits into
mainfrom
fix/disk-hygiene-resume-notice
Open

kyle-sexton wants to merge 6 commits into
mainfrom
fix/disk-hygiene-resume-notice

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Closes #9853

Summary

The /disk-hygiene:clean skill-frontmatter belt (PowerShell deletion prompts, deny-by-default Bash lane) is registered only in the process that invoked the skill. A Windows probe showed that after claude -p --resume it was gone, so a manual-lane delete the user had approved before the resume ran without a prompt. This PR implements the owner's decision D on the issue: a resume notice built from the existing hooks, plus doc corrections. It does not add a gate.

Fix

  • The existing UserPromptExpansion row for /disk-hygiene:clean now passes --marker-root ${CLAUDE_PLUGIN_DATA} --launch-marker clean-invoked to the launcher. The launcher already had this option (the guard-launch monitor uses it), and it writes clean-invoked/<session_id>.launched.
  • The existing SessionStart entry (same matcher group) gets a second, exec-form handler, hooks/resume-notice.mjs. On source: resume with that session's marker present, it returns additionalContext telling the model to make no delete or overwrite until the user types /disk-hygiene:clean again. In every other case it prints nothing. It is node only and spawns no bash or Python. Nothing is added on any per-tool-call path, and no new event or matcher is registered.
  • safety-model.md now gives the belt's lifetime as the invoking process and adds a verification record citing the #5609 probe. The stale "Not probed: the Windows PowerShell tool" line is replaced with what the probe found. unsupported-platform-handoff.md, the README and the setup skill state the resume limit.

Verification

  • node --test plugins/disk-hygiene/hooks/resume-notice.test.mjs: 8 pass. The notice fires only on a resume that has this session's marker. It stays quiet with no marker, with another session's marker, and on startup, clear, fork or compact. One end-to-end case runs the registered UserPromptExpansion row through the real launcher, then runs the registered SessionStart row.
  • scripts/affected-tests.sh --run over the diff, including run-python-hook.test.sh (it now pins the two SessionStart handlers and the expansion's marker flags), engine_context.test.sh and node-notice-rows.test.sh. All pass.
  • check-hook-exec-form, check-hook-slow-shapes, check-hook-userconfig-argv, check-hooks-description and check-killswitch-hoist: all clean.
  • Local code and security reviews (the CI review lanes read failing, so they ran here in fresh-context subagents): fixed a main-module guard that skipped main() when the plugin path is a symlink or junction (new test, which fails before the fix); corrected two wording points; noted that a marker survives a Python-less blocked expansion. Not fixed here: the launcher's predictable ${TMPDIR:-/tmp} marker fallback, a pre-existing pattern shared with the four guard-launch-monitor rows (low severity, tracked separately).
  • Left out: a live interactive claude --resume probe on Windows, which needs the fleet host. The notice keys on source: resume, so it covers interactive resume as well.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 25, 2026 11:58
…d is gone

The skill-frontmatter belt is not registered after claude -p --resume, so a
manual-lane delete approved before the resume ran unprompted. The
/disk-hygiene:clean expansion hook now writes a per-session marker, and the
existing SessionStart entry tells the model on a resume with that marker to
make no delete or overwrite until the user types /disk-hygiene:clean again.
The safety model and handoff docs state the belt's lifetime as observed.

Closes #9853

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Oct 25, 2026
@kyle-sexton

Copy link
Copy Markdown
Member Author

Held (do-not-merge) by Issue Triage: this body says it "implements the owner's decision D" and Closes #9853, but #9853 holds no owner decision (its only comment is the triage bot's). #9853 is a security question (resume hook as a real gate vs a model instruction) and goes to the owner. Lift the label once the owner answers on #9853, or once the body says Refs: #9853 and drops the decision-D claim and the owner agrees an interim merge is fine.

@kyle-sexton

Copy link
Copy Markdown
Member Author

#9853 is decided on the facts: option B. A SessionStart resume hook re-arms the belt in the resumed process, so the delete prompts again. This PR's notice (the "D" it cites) is a model instruction, not a gate, so it does not meet the decision. do-not-merge stays. Rework this branch to B, or close it and let the lane build B from #9853. The decision record will be posted on #9853 shortly.

@kyle-sexton kyle-sexton removed the do-not-merge Hard merge gate: do not merge while applied. label Oct 25, 2026
@kyle-sexton

Copy link
Copy Markdown
Member Author

Correction from Issue Triage: do-not-merge is lifted, and my earlier two comments are withdrawn. The owner decided #9853 as option D (Q111, 2026-10-25, "go w/ recommendations"; HITL Backlog is posting the record on #9853), so this PR's "decision D" claim stands. My "option B" premise was wrong: a SessionStart hook cannot register or re-arm a hook. Its output carries only additionalContext, initialUserMessage, sessionTitle, watchPaths and reloadSkills (https://code.claude.com/docs/en/hooks#sessionstart-decision-control).

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T02:31:13.236765Z f833fb4 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed a91f40c610fbce09e6cf9d155b58b374a93e7566: 0 finding(s), 0 as inline comments.

Incremental review of a91f40c against f833fb4: no findings.

What I checked

  • resume-notice.mjs: the new readPayload settles on the first chunk that parses as JSON, on the size cap, on the 2 s deadline, on end or on error. A partial JSON object never parses, so it cannot settle early on a truncated payload. setEncoding('utf8') keeps multi-byte characters intact across chunks. The finish guard stops it settling twice. main is now async, and the entry point exits through process.stdout.write('', () => process.exit(0)), so it exits 0 even when stdin stays open. Malformed or missing input still gives no output.
  • resume-notice.test.mjs: the new open-pipe test writes a payload without closing stdin, then asserts exit 0 in under 5 s and the notice on stdout. That covers the behaviour the change adds.
  • resume-notice.test.sh: a node-or-skip wrapper. scripts/affected-tests.sh pairs resume-notice.mjs with the sibling resume-notice.test.sh by file stem, and a changed .test.mjs selects the wrapper. The test-scope header names real files, so the stale-glob check should pass.
  • safety-model.md and setup/SKILL.md: the wording now says the belt is absent after a resume, based on the headless probe. It no longer asserts an unprobed /clear or compaction behaviour. I found no repo check that requires the removed "Verification record" label.

Not independently verified: I ran no tests. The pass counts in the PR body are the author's claims.

Minor, not counted as a finding: the size-cap and deadline-timeout paths of readPayload have no test, though it exports the maxBytes and deadlineMs options that would make them easy to add.

Spend: none; no billed resources or review-lane changes.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed a91f40c610fbce09e6cf9d155b58b374a93e7566: 0 finding(s), 0 as inline comments.

Security review (incremental since f833fb4)

No security findings in the changes since the last review. I read the incremental diff and the full head resume-notice.mjs. I did not run the tests. No manifest, hooks.json or trusted-path file changed in this increment, so there are no manifest <field> lines. The skip gate does not apply.

hooks/resume-notice.mjs

  • Stdin reader: the new readPayload replaces the blocking readFileSync(0). It stops at the first complete JSON value, at a 1 MiB cap, or after 2 seconds. Oversize or malformed input resolves to undefined, decide is not called, and nothing is printed.
  • Prefix parsing: a truncated JSON object cannot parse early. A prefix that parses to a primitive is falsy or fails the payload.source !== 'resume' check, so it yields no output. An attacker who controls stdin gains nothing, and the only input to decide is the host's own SessionStart payload.
  • Marker lookup: markerCandidates still reduces the session id to [a-zA-Z0-9_-], so a hostile session_id cannot traverse out of the marker directory. The non-printable-character rejection is unchanged.
  • Exit path: main(...).catch(() => {}).finally(() => stdout.write('', () => process.exit(0))) always exits 0 and flushes stdout first, so a failing or hanging stdin cannot block session start. The open-pipe test covers this.

resume-notice.test.mjs / resume-notice.test.sh: the new test spawns node with a scratch marker directory and kills the child afterwards. The wrapper runs node --test on a fixed path and skips when node is absent. Neither handles untrusted input.

safety-model.md and setup/SKILL.md: these are wording and pointer rewrites. They still say the belt is absent after a resume, that the plugin only gives a notice and not a gate, and that the model must follow the notice. No control is deleted or narrowed, so the instruction-surface lens finds nothing. The edit trims the /clear, fork and compaction sentence to "a session that starts under a new id gets no notice". That is a documentation narrowing only, and no enforcement depended on it.

One limit carries over from before this increment and is not new: the resume notice is advisory and depends on the model following it. The PR description states this openly.

Cost: nothing in this change can start a real charge.

critical-findings: 0

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f833fb423d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const markerRoot = at >= 0 ? (argv[at + 1] ?? '') : ''
let payload
try {
payload = JSON.parse(readFileSync(0, 'utf8'))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound the resume notice's stdin read

When the hook's input pipe delivers the JSON but delays EOF, readFileSync(0, 'utf8') blocks until EOF instead of processing the payload. This stalls even unrelated startup events and can consume the handler's entire 10-second timeout, discarding the resume notice when it is needed. A local subprocess check confirmed that a complete payload with stdin left open keeps this handler running until the pipe closes. Use a bounded stdin buffer and add a delayed-EOF regression test, following rule 3 of the hook-precision convention linked from AGENTS.md.

AGENTS.md reference: AGENTS.md:L128-L129

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding Classification Evidence
Bound the resume notice's stdin read VALID (fix now) readFileSync(0) waited for EOF. Fixed in a91f40c: readPayload stops at the first complete JSON payload, a 1 MiB cap or a 2 s deadline, and the process exits even with stdin open. A new test, "a payload whose pipe stays open is handled without waiting for EOF", writes the payload, leaves stdin open, and asserts the notice and an exit within 5 s. Against the old script, that test hangs.

Comment on lines +610 to +614
**The belt does not survive a resume.** `claude -p --resume <id>` starts a new process on the same
session id, and in that process the belt is not registered: the same PowerShell delete that the belt
prompted (and the headless run denied) inside one process ran unprompted after `--resume`, and the file
was recycled. With the belt gone, the deny-by-default Bash lane and the PowerShell deletion prompts are
gone too; only the always-on engine gate, the permission mode and the auto-mode classifier remain. The

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep upstream lifetime claims in pointer records

The new skill reference text restates Claude Code's hook-registration and resume behavior as standing facts, and the subsequent verification record repeats those claims with a pinned product version. AGENTS.md explicitly prohibits volatile upstream specifics in skill text even when accompanied by a dated record; its linked rule requires keeping the operational decision and a links-only pointer instead. Retain the instruction to re-run /disk-hygiene:clean before resumed mutations, but move the probe narrative and version details outside the skill and keep the exact upstream/probe pointers, as-of date, and recheck trigger. Apply the same correction to the changed handoff reference and setup skill.

AGENTS.md reference: AGENTS.md:L107-L109

Useful? React with 👍 / 👎.

…t to pointers

The notice no longer waits for EOF: it stops at the first complete JSON
payload, a size cap or a deadline. A sibling .test.sh lets CI's lanes run
the Node suite. The safety model's resume text states our decision with a
pointer record instead of restating upstream behavior and versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(disk-hygiene): the clean skill's PowerShell belt is gone after --resume, so a manual-lane delete runs unprompted

1 participant