Skip to content

fix(docs-hygiene): gate the porcelain rename split and unescape \\ (0.19.1) - #3171

Merged
kyle-sexton merged 2 commits into
mainfrom
claude/work-items-integration-cxy2xg
Aug 23, 2026
Merged

fix(docs-hygiene): gate the porcelain rename split and unescape \\ (0.19.1)#3171
kyle-sexton merged 2 commits into
mainfrom
claude/work-items-integration-cxy2xg

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Closes #3143

Summary

audit-noise's git status --porcelain parse dropped files whose paths git
treats specially. Both defects are the silent-false-negative class: the file
did not error, it simply disappeared from the target list, so a run over a tree
containing one reported clean.

Fix

Rename split is gated on the status letter, not the path text. It previously
fired on any record whose path contained " -> ", so a file literally named
notes -> draft.md was reduced to draft.md — a name that resolves to nothing.
It now gates on [RC] in either column, which is narrower and still catches a
rename recorded in the index or the worktree.

\\ is now unescaped as well as \". Git C-quotes a path for an embedded
backslash too, so back\-slash.md stayed escaped and resolved to nothing. Both
escapes are undone, \" before \\.

#3140 landed on main while this branch was in flight. The gate and the
two-step unescape here are identical to the ones it gave
code-tidying/audit-comment-residue, so the two porcelain parsers now converge
rather than failing in opposite directions on renames — which is what #3143
asked for.

audit-noise's SKILL.md does not mirror this parse — it uses a plain
grep '\.md$' pipeline — so the conditional "mirrored parser" half of the issue
does not apply here.

docs-hygiene 0.19.1.

Verification

  • detect.test.sh: all 66 checks pass, including 5 new cases.
  • Discriminator check: reverting only detect.sh to the old parse fails
    exactly the two new defect cases (' -> ' path, backslash path) and nothing
    else — so neither case passes vacuously. The rename cases pass under both
    implementations by design; they are regression guards showing the new gate
    does not cost the old -> new handling it narrows.
  • All 7 docs-hygiene suites pass, re-run after merging main; nothing outside
    this skill references audit-noise/scripts/detect.sh.
  • shellcheck, shfmt -d, markdownlint-cli2, typos, editorconfig-checker: clean.
  • check-fixture-git-isolation.sh --check, check-plugin-manifest-presence.sh,
    check-changelog-parity.sh --check / --check-order / --check-bump /
    --check-preserved against origin/main: pass.

The suite also picks up the unset GIT_DIR GIT_WORK_TREE GIT_CONFIG isolation
line that 0.18.3's sweep missed on this file.

Known residual, recorded at the parse site rather than left implicit: git's
octal escapes (\NNN) for control and non-ASCII bytes are still not decoded, so
those paths continue to miss. Converging on git status --porcelain -z would
close the class outright rather than extending the string parse a third time.

Related

….18.4)

Two silent-false-negative defects in `audit-noise`'s `git status --porcelain`
parse: a file that should have been audited simply dropped out of the target
list.

The rename split fired on any record whose path contained " -> ", not only on a
rename, so a file named `notes -> draft.md` was reduced to `draft.md` — a name
that resolves to nothing. It is now gated on the status letter in either column
([RC]), which is narrower and also catches a rename staged in either column.

The unquote step undid \" but not \\. Git C-quotes a path for an embedded
backslash too, so `back\-slash.md` stayed escaped and resolved to nothing. Both
escapes are now undone, \" before \\.

Regression cases cover a path containing a literal " -> " and a path containing a
backslash; a plain path passes either implementation, so neither is redundant. A
genuine rename case shows the new gate does not cost the old -> new handling it
narrows. Verified by reverting detect.sh to the old parse: exactly those two
cases fail, and the rename guards pass on both.

The suite also picks up the `unset GIT_DIR GIT_WORK_TREE GIT_CONFIG` isolation
line that 0.18.3's sweep missed here.

Known residual, recorded at the parse site: git's octal escapes (\NNN) are still
not decoded. Converging on `git status --porcelain -z` would close the class
outright rather than extending the string parse a third time.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXYVz59XVYS7NEjMB5UyHq
Resolves two conflicts in docs-hygiene, both from #3142 bumping the plugin to
0.19.0 while this branch carried 0.18.4:

- plugin.json: version becomes 0.19.1, this branch's fix on top of 0.19.0.
- CHANGELOG.md: this branch's entry re-headed [0.19.1] and ordered above
  [0.19.0]; both entries kept in full.

#3140 also landed on main during this branch's life, so the sibling
`code-tidying/audit-comment-residue` parse it ports from is now present. The
gate and the two-step unescape here are identical to it, so the two porcelain
parsers converge rather than failing in opposite directions on renames. Noted
in the changelog entry.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXYVz59XVYS7NEjMB5UyHq
@kyle-sexton kyle-sexton changed the title fix(docs-hygiene): gate the porcelain rename split and unescape \\ (0.18.4) fix(docs-hygiene): gate the porcelain rename split and unescape \\ (0.19.1) Aug 23, 2026
@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 58s —— View job


Security review

  • Gather context (PR diff, changed files)
  • Apply skip gate
  • Review changed files for security-relevant issues
  • Post findings / conclusion

No security issues found.

This PR touches plugins/docs-hygiene/skills/audit-noise/scripts/detect.sh (and its test), a read-only local audit script that parses git status --porcelain output from the current working repo. Reviewed the full diff against the security criteria (injection, trust-boundary, auth/access-control, GitHub Actions, secrets handling):

  • No eval, bash -c with concatenated strings, or other dynamic code execution is introduced. The status-letter gate ([[ "${line:0:1}" == [RC] || "${line:1:1}" == [RC] ]]) and the added backslash-unescape (${local_path//\\\\/\\}) are both plain Bash parameter expansions/pattern matches — no shell metacharacter interpretation of the parsed path.
  • All array/variable usages remain properly quoted ("$target", "${TARGETS[@]}", "$local_path") both before and after the change, so a maliciously-named file (e.g. containing `, $(), or ;) still can't reach command injection.
  • The C-quote unescaping only undoes \" and \\; it doesn't decode octal escapes (\NNN), and the PR explicitly documents this as a known residual (comment at plugins/docs-hygiene/skills/audit-noise/scripts/detect.sh lines 139–142) rather than a silent gap — no path-traversal implication since / isn't part of git's C-quote escape set and porcelain paths stay repo-relative.
  • detect.test.sh adds unset GIT_DIR GIT_WORK_TREE GIT_CONFIG (line 7) to prevent an inherited git env from redirecting fixture git init/git config calls into the CI runner's actual repo — a good isolation hardening, not a regression.
  • plugin.json / CHANGELOG.md changes are metadata/version-bump only.

No GitHub Actions workflows, auth logic, credential handling, or network-facing input parsing are touched by this PR — skip-gate condition 2 (no security-relevant surface) applies.

@github-actions

Copy link
Copy Markdown

Last security-reviewed head: 22fb51108b7d9c54a47521e9c324e9c78d05e791. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 5m 1s —— View job


Code review complete

  • Gather context (PR diff, changed files, CLAUDE.md)
  • Run skip-gate checks
  • Review detect.sh changes (rename-split gate, \\ unescape)
  • Review detect.test.sh new cases
  • Review CHANGELOG / plugin.json version bump
  • Post findings

No blocking findings.

What I checked

  • Rename-split gate (detect.sh#L128-L132): [[ "${line:0:1}" == [RC] || "${line:1:1}" == [RC] ]] correctly gates the " -> " split on the porcelain status letter rather than the path text. Traced through the untracked/plain, ordinary-modified-with-literal-" -> ", and genuine-rename cases by hand against the porcelain format — each resolves the way the PR describes.
  • \\ unescape (detect.sh#L138-L141): ${local_path//\\\\/\\} is the same idiom already shipped in the sibling code-tidying/audit-comment-residue fix (commit 9a6c649, now on main and merged into this branch), applied in the same order (\" before \\). That sibling fix landed with its own passing regression suite, which gives strong independent corroboration that this exact construct behaves as intended — I did not have a working Bash execution path in this sandbox (script/test-suite invocations were denied approval) to run detect.test.sh myself, so I'm not claiming to have independently executed it; the "66 checks pass" and discriminator-revert results in the PR body are author-claimed, not something I verified by running the suite.
  • Test coverage: the three new cases (literal " -> " in an untracked path, a genuine rename still resolving to the new path, and a backslash path) target the two defect classes precisely, and the backslash case is defensively skipped when the filesystem rejects the character. The unset GIT_DIR GIT_WORK_TREE GIT_CONFIG isolation line matches the convention from Drain scripts/fixture-git-isolation-baseline.txt (36 grandfathered suites) #2872.
  • CHANGELOG.md / plugin.json: version bump (0.18.30.18.4) and changelog entry are consistent with the fix.
  • Scope claim on SKILL.md: confirmed audit-noise/SKILL.md line 16 is a plain grep '\.md$' pipeline unrelated to the rename/quote parsing in detect.sh — the "mirrored parser" concern genuinely doesn't apply here, unlike the code-tidying sibling where the SKILL.md pre-computed-context line carried the identical parse and needed the same fix.

One non-blocking note: the PR body's "Related" section says #3140 has not landed, but that commit (9a6c649) is now present on this branch via the origin/main merge (22fb511) — purely a description-accuracy point, not a code issue, and doesn't affect the diff's correctness.

@github-actions

Copy link
Copy Markdown

Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count.

Copy link
Copy Markdown
Contributor Author

Thanks — no blocking findings on either lane, and the hand-trace of the three porcelain cases matches what the suite asserts.

Two points in the review read a stale snapshot of this PR and are already resolved on the current head (22fb5110):

On the execution caveat: correct that you couldn't run the suite, and treating "66 checks pass" as author-claimed is the right posture. For the record, the load-bearing evidence isn't the pass count — it's the discriminator: reverting only detect.sh to the old parse fails exactly the two new defect cases (' -> ' path, backslash path) and nothing else. The rename cases pass under both implementations by design, so they're regression guards rather than evidence the fix works.


Generated by Claude Code

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

No description provided.

@kyle-sexton
kyle-sexton merged commit 8ae62b5 into main Aug 23, 2026
52 checks passed
@kyle-sexton
kyle-sexton deleted the claude/work-items-integration-cxy2xg branch August 23, 2026 07:21
kyle-sexton added a commit that referenced this pull request Aug 23, 2026
…paths (0.20.1) (#3187)

0.19.1 (#3171) fixed the porcelain parse in audit-noise's detect.sh and left
SKILL.md's `Uncommitted .md files:` pre-computed-context line untouched. That line
previews the same discovery with a grep rather than with the parse, so it shared the
defect class without sharing the code, and survived the fix that removed the class
everywhere else.

Git C-quotes any path it treats specially, and a quoted porcelain record ends with the
closing quote, not `.md`. `grep '\.md$'` therefore matched nothing for `my notes.md`,
`notes -> draft.md`, or `back\-slash.md`, and the preview reported them absent with no
signal — the same silent false negative, reaching the model one surface earlier.

The regression test extracts the grep out of SKILL.md and executes it rather than
restating it, since a restatement keeps passing while the real line rots.

Carries forward the SKILL.md half of #3168, whose detect.sh half landed via #3171.

Refs #3143
kyle-sexton added a commit that referenced this pull request Aug 23, 2026
…#3151)

No related issue: follow-on to the limitation #3140 recorded at its own
parse site. #3126 is already closed by that PR, and no issue tracks the
residual escape-decode gap.

## Summary

#3140 fixed the #3126 false negative by slicing the v1 porcelain record,
and recorded the remaining limitation at the parse site:

> Git's octal escapes for control and non-ASCII bytes are still not
decoded by either, so such a path continues to miss.

This PR closes that gap, in both parsers that carry it. **It is a
follow-on to #3140, not a competing fix.**

| Path | v1 renders as | v1 slice yields |
|---|---|---|
| `café.py` | `?? "caf\303\251.py"` | literal escape sequence — names
nothing |
| `tab<TAB>here.py` | `?? "tab\there.py"` | literal `\t` — names nothing
|

Both confirmed against git 2.55. The file is silently dropped, so the
run reports a clean tree — the same false-negative class #3126
described.

## Fix

Both porcelain parsers in this skill move to the NUL-delimited
`--porcelain -z` form, which git documents as performing no quoting or
backslash-escaping, so there is nothing left to decode.

- **`detect.sh`** — the audit's target router.
- **`SKILL.md`'s `Uncommitted code files:` preview** — the pre-computed
context the model reads. #3140 deliberately brought this to parity and
added a test that extracts and runs it, so leaving it behind would have
reopened the divergence that test exists to prevent: the audit would
find `café.py` while the preview listed nothing.

Under `-z` a rename emits the **new** path first and the original as a
following record — the reverse of v1's display order — and that second
record is consumed and dropped. Rename handling is therefore structural,
with no arrow matching, which also resolves the intent-to-add rename
#3140 gated on the worktree status letter (` R dst\0src\0` → `dst`,
verified against git 2.55).

## Verification

- **53/53 pass**, including every one of #3140's checks.
- Nothing vacuous, checked by reverting each piece:
- `main`'s `detect.sh` → fails 2 of case 11's 5 assertions (`non-ASCII`,
`tab-bearing`). The arrow assertion passes on `main` — #3140's `[RC]`
gating already handles it, **not** claimed here.
- `main`'s `SKILL.md` → fails 3 assertions, including #3140's own
`SKILL.md preview covers every file detect.sh audits`.
- The parity harness was itself masking the defect and is fixed here. It
extracted only the awk program and hardcoded the porcelain invocation;
feeding `-z` to a v1 program makes the v1 program look correct, because
`-z` output carries no quoting for it to fail at decoding. It now reads
the invocation from `SKILL.md` too, and `REPO13` gains a non-ASCII
fixture.
- `mawk 1.3.4` (the runner's default `awk`) confirmed to support `RS =
"\0"` before relying on it.
- Local gates green: typos, shell-portability (scripts and `SKILL.md`),
skill-portability, skill-precompute-compose, changed-skills, leaf-names,
count-claims, fixture-git-isolation, orphaned-fixtures,
manifest-duplicate-keys, `bash -n`, changelog-parity in all three modes.
- `ci-status` (the required aggregate check) reported **success** on
head `65a7b2c`; head `d7b2b67` adds only a `main` merge plus the version
rebase.

## Current state — read this before continuing

Head is **`d7b2b67`**, version **`0.14.2`**. `main` published its own
`0.14.0` (#3156) and `0.14.1` mid-review, so the version was rebased
twice; the changelog keeps each published entry intact with this PR's
entry above them.

**One blocker remains, and it is not about the code.** The branch's
commits are signed with a key that is not registered on the committing
GitHub account, so they report `verified=false, reason=unknown_key`.
That trips two ruleset rules:

- `required_signatures`
- `require_extra_approval_for_unattributed_changes`

Everything else is satisfied: all four required checks green, all review
threads resolved, no merge conflicts, and
`required_approving_review_count` is **0** — so once the signature
question is settled this PR needs no human approval.

Two ways to settle it:

1. **Register the signing key** on the account (Settings → SSH and GPG
keys → New SSH key, type *Signing Key*). No push needed; the existing
commits become verified, and the commit history is preserved.
2. **Rebuild the branch through the GitHub API.** Commits created via
the API are signed by GitHub and verify automatically — that is why
every commit on `main` shows `committer=noreply@github.com,
verified=true`. Since the repo is squash-merge only, collapsing this
branch's commits loses nothing that would survive the merge anyway.

## Related

- Refs #3140 — landed the v1 slice this builds on; its `0.13.3` entry
and full test suite are preserved here.
- Refs #3156 — bumped `code-tidying` to `0.14.0` mid-review.
- Refs #3126 — the original bug, closed by #3140. Not reopened here.
- Refs #3164 — `audit-noise` carries the same defect class. #3171 fixed
its rename-split half; the octal-escape half is still open and tracked
there. **Not fixed here** — separate plugin, separate version bump.
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(docs-hygiene): audit-noise porcelain parse mangles paths containing " -> " and leaves \\ escaped

2 participants