Skip to content

Read a Backslash-Newline by Its Quoting in the Write Guard - #3357

Merged
ptr727 merged 6 commits into
developfrom
feature/guard-backslash-newline-quoting
Oct 22, 2026
Merged

ptr727 merged 6 commits into
developfrom
feature/guard-backslash-newline-quoting

Conversation

@ptr727

@ptr727 ptr727 commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Narrows the write guard's backslash-newline denies, which since the last round read every backslash-newline in a command naming git or gh, quoting and heredoc data included.

  • A scan, _line_break_contexts, maps each backslash-newline to its context.
  • Unquoted, inside double quotes, in an unquoted heredoc, at a comment's end, and wherever the scan does not finish, the split-word and escaped-line-break denies stand as before.
  • A single unquoted backslash-newline between a command separator (&&, ||, |, |&, ;, &) and a non-operator character splits no word, so it is left to the fold.
  • Inside single quotes, or ending a quoted heredoc data line not followed by its delimiter, the outer shell keeps the backslash-newline as text. _literal_span_kept keeps such a span as text only where the hook can read it as a shell running it would. A single-quoted span must be its word's only quoted, escaped, or expanded part. A span naming git, gh, or a push must hold no further quoting, with each backslash-newline in its own command text or ending a comment there. A span not kept is read as develop reads it.
  • For a kept span, the command is classified again with each backslash-newline as the span's own scan places it, with each joined, and with each a line break, and denied where any reading is.
  • The README's requirement 4 paragraph describes the cases the denies skip.

Refs #3344. This fixes these items from #3344:

  • "Over-Denials the Change Introduces": all three, a single-quoted backslash-newline, a Markdown hard line break in a quoted heredoc inside "$(cat <<'EOF' ... EOF)", and git commit -F- <<'EOF' data ending in \.
  • The fourth-pass item on _SPLIT_WORD matching an operator glued to a line continuation, git add x &&\ and git log --oneline |\.

The other items in #3344 are not addressed here.

Verification

  • Self-test passes. New cases cover each intended allow and a deny for each shape that must still deny: a split inside double quotes, in an unquoted heredoc, before a quoted heredoc's delimiter, inside $'...', an &\-newline-& split operator, a redirection operator, single-quoted shell bodies (joined, escaped backslash, comment-ending, mixed comment-ending and splitting), quoted heredocs fed to a shell, a further quoting level, a '\''-joined word, and a span after an escaped word break.
  • Revert proofs: with the narrowing replaced by develop's whole-text regexes, the new allow cases fail. With no extra readings, the six shell-body deny cases fail. With the line-break reading removed, the two comment-ending cases fail. With only the joined and line-break readings, the three mixed cases fail. With every span kept, the nesting and word-adjacency cases fail.
  • Differential: every distinct command string the self-test and test_install.py pass to classify, plus constructed probes from this lane and its local review passes, classified under develop's guard and this branch's with identical offline seams. 10560 distinct commands: 3889 deny/deny, 6591 allow/allow, 78 deny to allow, 2 allow to deny, 0 errors.
    • The recorded self-test and test_install.py corpus flips only on this branch's seven new allow cases.
    • Every probe-only deny-to-allow flip is an intended form or a direct sibling (single-quoted data, quoted-heredoc data including Markdown bodies with apostrophes, a continuation glued to a separator), or a command whose plain form develop already allows (filed below, or harmless, such as a ref holding a backslash or a word with a plain suffix).
    • The two allow-to-deny flips are a spaced comment-ending backslash-newline in a bash -c body, which develop allowed while denying its plain form.
  • test_install.py, prose_lint.py (diff scope, sentence-length included), repo_gate.py --check eol and eol-coverage, spec/validate.py, ruff, and the markdownlint, cspell, and editorconfig-checker Docker linters are clean.

Known Remaining Findings

The local review reached its cap of six passes. The last two passes stopped before probing, so their probe phase ran in the lane itself over the separator and kept-span paths, with no new flip. These introduced findings remain, each a wording or over-deny item rather than an allow. Each is filed in #3358, since a fix push would need a pass past the cap:

  • introduced, docs: the README states the word rule as "its word's only quoted part", where the code also refuses an escaped or expanded part, and states the names test as "git or gh", where the code also tests a push in each reading.
  • introduced, docs: _literal_span_kept's summary line reads as if every kept span is read as its inner scan places it, which its later lines qualify for a span naming no git or gh.
  • introduced, over-deny: for an escaped backslash before a kept newline, every reading drops the backslash pairs, where a shell keeps one literal backslash per pair. This can only make a reading deny more.
  • introduced, residual over-deny: a quoted span naming git or gh that also holds an apostrophe, double quote, backquote, or << is read as develop reads it. A Markdown PR body mentioning git in a code span and holding a hard line break is still denied, as on develop.

Filed

Pre-existing gaps the local review found, each allowed on develop in its plain form: #3351 (a cd that does not lead the command, for rule 6), #3352 (a trap action string), #3353 ()# and <<- tab stripping), #3355 (a comment-ending backslash-newline at top level, and an eval body, for rule 6), #3356 (text piped into a shell, or produced by $(...) as a -c body). Design: #3354 (consolidate the guard's context scanners).

#3341 Confirmation

BASH_BIN="$(command -v bash)"; git push --tags origin and bash -c 'command -v sh'; git push --tags origin both classify allow on develop 201cbe7 with offline seams.

🤖 Generated with Claude Code

ptr727 and others added 3 commits October 22, 2026 07:02
The split-word and escaped-line-break denies read every backslash-newline
in a command naming git or gh, so text that splits no word was denied: a
single-quoted body, a Markdown hard line break in a quoted heredoc, and a
continuation glued to a command separator such as && or |.

A scan now records the context of each backslash-newline. Unquoted, inside
double quotes, and in an unquoted heredoc the deny stands, except directly
after a command separator. Inside single quotes or quoted heredoc data the
outer shell keeps it as text, so the command is classified a second time
with those joined, as an inner shell running that text would read them,
and denied where that reading is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README listed the contexts the denies read, which left out a comment's
end, an ANSI-C quote, a substitution, and a quoted heredoc line before its
delimiter. It now names the cases the denies skip instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shell running a quoted body ends a comment at a backslash-newline rather
than joining the next line into it, so the joined reading alone hid a
command after a commented line. The command is now also read with each
quoted backslash-newline as a line break, and denied where either reading
is. A continuation glued to a command separator is left to the fold, which
already reads the same words, so it no longer costs a second reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 22, 2026 14:59
@ptr727

ptr727 commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 9d5cfe3dcfa17dbb580a9913d2a86466beacd491, the content this pull request carries at that commit against develop, and it recorded 3 findings.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The joined and line-break readings were all-or-nothing, while a shell
running a quoted body decides each backslash-newline separately. A body
holding a comment-ending one and a word-splitting one matched neither.
The quoted text is now scanned as the script a shell may run, and a third
reading takes each backslash-newline as that scan places it.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The guard can allow a command when an inner shell mixes continuation readings, and the README needs clarification.

2 open issues
What changed in this PR

This pull request narrows write-guard denials for backslash-newlines by recognizing shell quoting, heredocs, comments, separators, and nested shell readings.

Changes:

  • Adds context-aware scanning and alternate backslash-newline readings.
  • Recursively reclassifies quoted heredoc and nested shell-body content.
  • Updates the safety README and expands regression coverage.
File Reviewed changes
host-setup/​agent-safety/​README.md Documents the revised quoting and heredoc behavior. Nit (P2): Clarify command-substitution and ANSI-C exceptions to match the scanner.
host-setup/​agent-safety/​claude/​gh-write-guard.py Implements contextual scanning, recursive readings, and regression tests. Critical (P1): Preserve independent inner-shell readings for each literal continuation.

Review effort: Lite

Comment thread host-setup/agent-safety/README.md Resolved
@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.32%. Comparing base (201cbe7) to head (494a690).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff              @@
##           develop    #3357       +/-   ##
============================================
+ Coverage    65.53%   81.32%   +15.78%     
============================================
  Files           18       18               
  Lines        10036    10045        +9     
============================================
+ Hits          6577     8169     +1592     
+ Misses        3459     1876     -1583     
Flag Coverage Δ
python-3.13 81.32% <ø> (+15.78%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ptr727 and others added 2 commits October 22, 2026 08:14
The per-span reading resolved one shell level, so a span holding further
quoting, or a single-quoted part joined to other quoted parts of its word,
could hide a split word from every reading. A span is now kept as text only
where its word has no other quoted part, and, where it names git or gh, it
holds no further quoting and each backslash-newline sits in its command
text or ends a comment there. Any other span is read as develop reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An escaped space before a single-quoted span joins the text before it into
the same word, so the span's own scan misreads the script the inner shell
runs. Such a span is no longer kept as text.

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

ptr727 commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 494a6908bfacb84f77fcf6f8021975a3ed7d547b, the content this pull request carries at that commit against develop, and it recorded 5 findings.

@ptr727
ptr727 merged commit e0a6dfc into develop Oct 22, 2026
13 checks passed
@ptr727
ptr727 deleted the feature/guard-backslash-newline-quoting branch October 22, 2026 16:12
ptr727 added a commit that referenced this pull request Oct 22, 2026
…dows Self-Test Skip, and Subprocess Failure Rule to Main (#3359)

## Summary

- Skip the invalid-byte audit self-test probe when Windows Git cannot
add the name, so the self-test no longer fails on a host that cannot
represent the path (#3350). The skip fires only on that one add, after a
control file's add has proven git works (#3360).
- Read a backslash-newline in the write guard by its quoting: inside
single quotes and quoted heredoc data it is text, and a continuation
after a shell operator is no word split (#3357).
- Keep each escaped backslash pair before a kept line break in the
guard's readings, as the shell does, and state the kept-span word and
names tests in full in the README and docstring (#3368).
- Add a `CODESTYLE.md` General rule: classify a subprocess failure by
which command failed and the status code it reports, never by the
wording of its message (#3366).

Closes #3338
Closes #3358
Closes #3363

Refs #3344, which stays open because only part of it is fixed here.
#3358's Markdown-body finding moves to #3354.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

2 participants