Skip to content

Read a Heredoc on a Line Holding (( Every Way a Shell Might in the Wait-Loop Rule - #2114

Merged
ptr727 merged 7 commits into
developfrom
feature/auto-2059
Sep 30, 2026
Merged

ptr727 merged 7 commits into
developfrom
feature/auto-2059

Conversation

@ptr727

@ptr727 ptr727 commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Rule 7 of gh-write-guard.py stripped no heredoc body on a line holding ((, so a comparison in a real heredoc's body could vouch for a wait loop whose own condition carried no bound. This reads such a line once as opening nothing and once per << whose tag the rule accepts and whose non-empty body a later line closes, and denies where any reading holds an unbounded wait.

  • Merged with develop's two-reading opener (Read the Guard's Token Fallback With a Bash-Aware Context Scan #2076). The forks run on the comments-dropped _context_lex reading, gated on the #-as-text reading finding an opener, and the _operator_tokens line reader forks the same way. Both reading sets are judged together.
  • Reading limit, per decision Decide How the #2059 Heredoc Fix Proceeds After Its Review Budget #2066. Past 16 readings the command is denied unread only when it names both sleep and a loop keyword, the keyword matched in its case as bash reads one. A << whose body is empty forks nothing, since that reading is the same text as the one stripping nothing.
  • Unread allow. A command naming no sleep once its line continuations, quotes, backslashes and $ signs are removed is allowed without building readings, which covers sl$'e'ep.
  • Module docstring item 7 and host-setup/agent-safety/README.md requirement 7 state the same.

Known residual

Past the limit, a lowercase while or until anywhere in the text counts, heredoc bodies and quoted arguments included, so five numbered heredoc writes whose prose says "retry until green" followed by sleep 2 are denied. That is the safe direction and follows from the keyword gate #2066 chose.

Filed from the local review passes

Pre-existing on develop, each constructed and reproduced there: #2111, #2112, #2113, plus new shapes commented onto #2062 and #2063.

Verification

--selftest passes, with a revert of each fix failing its new row. unittest discover -s tests (1856 OK), ruff format and check, mypy, prose lint against origin/develop, the eol gate, spec/validate.py and build_dist.py --check are clean. Three local-strict-review passes were recorded, the third reporting no regression across about 15,000 generated commands compared against develop.

Closes on promotion: #2059

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Safety checks now account for multiple possible heredoc interpretations when evaluating commands. Commands with an unbounded wait are denied if any interpretation contains one; ambiguous commands that exceed the reading limit are denied when they include both sleep and a loop keyword.
  • Documentation
    • Updated the Requirement 7 description to explain how these cases are evaluated.

ptr727 and others added 7 commits September 28, 2026 23:48
A line holding `((` beside a `<<` was never read as a heredoc opener, so a
real heredoc's body stayed in the command the wait-loop rule reads, and a
comparison in that body vouched for a loop whose condition had no bound.

Such a line now forks the text into one reading per way it can open,
including none, and the wait-loop rule denies when any reading holds an
unbounded wait. Past 64 readings, a command that sleeps is denied.
A strip reading with no closing line dropped the rest of a quoted payload
holding a shift, and the loop before it was then denied. The reading limit
now counts the reading in progress, falls to 16, and past it denies any
command naming `sleep` once its quotes are removed, which a quote pairing
across two bodies can no longer hide. A command naming no `sleep` is
allowed before any reading is built.
The fork sentence now names the tag test, since a tag the rule rejects
forks nothing, and the body-end docstring names the index it returns.
Resolve the unbounded-wait rule against the two-reading heredoc opener
from develop. The per-`<<` forks on a `((` line now run on the
comments-dropped context-lex reading, gated on the `#`-as-text reading
finding an opener, and the line-reader fallback forks the same way.
Both reading sets are judged together under the reading limit.

This also carries the round decision #2066 settled. A `<<` whose body
is empty forks nothing, and the limit deny needs a loop keyword as
well as `sleep`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unread-allow text now drops `$` beside quotes and backslashes, since
the lexer reads `sl$'e'ep` as `sleep`. The past-limit deny matches a
loop keyword in its case, so a capitalized word in a heredoc body no
longer counts as one. The docstring and README now say a fork needs a
non-empty body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lexer deletes a backslash-newline, so the text the unread allow
reads now deletes one too, and its docstring claim holds for a direct
caller as well as for classify, which folds them first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 30, 2026 01:31
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: ptr727/ProjectTemplate/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 55abb18e-7e31-493b-b649-1acb54a7ec55

📥 Commits

Reviewing files that changed from the base of the PR and between 4c9087d and d16f444.

📒 Files selected for processing (2)
  • host-setup/agent-safety/README.md
  • host-setup/agent-safety/claude/gh-write-guard.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The wait guard now evaluates multiple possible heredoc readings for ambiguous arithmetic-shift lines. It denies commands when a reading contains an unbounded wait, and applies a text-based denial condition when the reading limit is exceeded.

Changes

Wait guard

Layer / File(s) Summary
Evaluate ambiguous heredoc readings
host-setup/agent-safety/claude/gh-write-guard.py, host-setup/agent-safety/README.md
The guard discovers possible heredoc openers and evaluates up to 16 readings. It denies commands when any reading contains an unbounded wait. When the reading limit is exceeded, it denies commands whose text contains both sleep and a loop keyword. Documentation and self-tests cover ambiguous readings and limit behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to d16f4

The change strengthens wait detection for ambiguous heredocs. No actionable merge-blocking issue is established; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to d16f4

The change favors denying unsafe waits and remains within the existing command-safety gate, without visibly adding credentials, service access, or execution authority. Risk is low rather than minimal because the baseline comparison and complete shell-syntax coverage remain unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The directly affected security outcome is whether a Bash command may start a persistent, unbounded sleeping loop on a host where this hook is installed. The inspected path adds no credential or cross-service authority. Exposure through other execution channels and installed deployment wiring was not established.

Security Findings and Attack Paths

  • inferred — The targeted evasion places a comparison inside heredoc data so that it appears to bound a loop whose executable condition has no bound. Alternative stripped readings address this misleading interpretation, and added regression cases expect denial. This is the documented repair target, not a newly verified exploit or a claim of complete parser coverage.

Trust Boundaries and Controls

  • observed — Agent-supplied command text crosses into the host-side enforcement decision without being executed by the new parsing helpers. Normalized text lacking sleep skips Rule 7 analysis; overflow with sleep and a supported loop keyword denies. These are explicit control decisions, not identity or privilege grants.
  • observed — The inspected entrypoint ignores malformed JSON and non-Bash event shapes rather than issuing denial. Those paths are outside the supplied changed ranges. Their behavior should not be treated as proof that every execution channel is protected; no PR-induced expansion of those paths was identified.

Resilience and Maintainability Implications

  • observed — The overflow fallback searches normalized text without distinguishing executable syntax from prose. Consequently, lowercase loop words in heredoc data can contribute to denial when sleep is also present. The PR explicitly documents this conservative false-denial behavior rather than silently choosing one reading after exhaustion.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: evaluating heredoc readings for lines containing (( in the wait-loop rule. It is somewhat long, but it is specific and related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 1 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@codecov

codecov Bot commented Sep 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.47%. Comparing base (4c9087d) to head (d16f444).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #2114   +/-   ##
========================================
  Coverage    56.47%   56.47%           
========================================
  Files           16       16           
  Lines         7455     7455           
========================================
  Hits          4210     4210           
  Misses        3245     3245           
Flag Coverage Δ
python-3.13 56.47% <ø> (ø)

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.

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.

Copilot review overview

🔵 Needs a closer look

It reworks the core detection path of a security-critical write guard with intricate combinatorial heredoc-reading logic that warrants human verification even though no concrete defect was found.

Review effort: Balanced
Findings: None

What changed in this PR

This PR closes a false-allow in rule 7 of gh-write-guard.py (the agent-safety write guard that denies unbounded shell wait loops). Previously, any command line whose tokens held (( was skipped by the heredoc-body stripper, so a real heredoc's body on such a line was kept as commands, and a comparison inside that body could vouch for a wait loop whose own condition carried no bound (issue #2059). The fix replaces the single-reading strip with a fork-based reader (_heredoc_readings) that produces every plausible heredoc-strip reading and denies if any of them holds an unbounded wait.

Changes:

  • Replaces _heredoc_opener/_line_heredoc_opener/_strip_heredoc_bodies with _heredoc_openers/_line_heredoc_openers/_heredoc_readings, which read a (( line once as opening nothing and once per << whose non-empty body a later line closes, capping total readings at _HEREDOC_READING_LIMIT (16).
  • Adds a fast-path in _check_unbounded_wait: allow unread when the sanitized text names no sleep; past the reading limit, deny only when the text also names a loop keyword (_NAMES_A_LOOP), matching bash's case sensitivity.
  • Adds 14 self-test rows and updates module docstring item 7 plus README requirement 7 to describe the fork reading and limit-deny behavior consistently.
File Description
host-setup/​agent-safety/​claude/​gh-write-guard.py Fork-based heredoc reader, sleep/loop fast-path, reading limit, and new self-test rows implementing the #2059 fix
host-setup/​agent-safety/​README.md Requirement 7 prose extended to describe the ((-beside-<< fork reading and the reading-limit deny

Notes from the review: no dangling references remain to the removed helpers; the fork explorer is correctly bounded and its per-fork kept + term snapshots are aliasing-safe; the first/others invariant (a real opener only strips on non-(( lines, forks only on (( lines) holds; the sleep fast-path and _NAMES_A_LOOP casing are over-approximations in the safe (deny) direction and introduce no false-allow regression versus develop; docstring item 7 and README requirement 7 now agree with the code. The known residuals (multiple heredocs per (( line, a loop keyword inside a body/quote past the limit) are pre-existing and filed separately (#2112, #2111, #2113), acknowledged in the PR as safe-direction limitations.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ptr727

ptr727 commented Sep 30, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ptr727
ptr727 merged commit 6ee0bc7 into develop Sep 30, 2026
11 checks passed
@ptr727
ptr727 deleted the feature/auto-2059 branch September 30, 2026 02:09
ptr727 added a commit that referenced this pull request Sep 30, 2026
…y a Shell Might in the Wait-Loop Rule (#2115)

## Summary

Promotes develop to main, carrying
[#2114](#2114).

That PR makes the guard's unbounded-wait rule read a line holding `((`
beside a `<<` every way a shell might, once as opening nothing and once
per `<<` whose non-empty body a later line closes, so a real heredoc's
body can no longer vouch for a loop whose own condition carries no
bound. It merges that design with develop's two-reading opener, denies
past a 16-reading limit only when the command names both `sleep` and a
loop keyword per decision #2066, and allows a command naming no `sleep`
without building readings. Known residual: past the limit a lowercase
`while` or `until` in heredoc prose still counts, which is the safe
direction.

## Review coverage

Copilot and CodeRabbit each reviewed #2114 through its final head
`d16f444d` with no findings. This promotion carries byte-identical
content, checkable with two commands:
- `git rev-parse d16f444^{tree} 6ee0bc7^{tree}` prints `aecb62d8...`
for both.
- `git diff origin/main origin/develop | git hash-object --stdin` and
`git diff 4c9087d d16f444 | git hash-object --stdin` both print
`20c0428e...`.

Closes #2059

🤖 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