Skip to content

fix(shell): judge only added lines in the changed-script allow list check - #1071

Open
tbhb-agent wants to merge 3 commits into
mainfrom
fix/shell-allowlist-changed-lines-20261102
Open

tbhb-agent wants to merge 3 commits into
mainfrom
fix/shell-allowlist-changed-lines-20261102

Conversation

@tbhb-agent

@tbhb-agent tbhb-agent commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

With --changed BASE, shell-allowlist now reports a finding only when its line is one a change adds relative to the merge base. A script with no history, such as an untracked file, counts in full. The explicit path mode and --summary still scan whole files.

Why

The changed-script gate parsed every touched shell file and reported every unlisted command in it. A long script that already ran awk or python3 failed on any edit, even when the edit added only allowed commands. packages/agents/source/lib/lib.sh has many such findings. No change to it could pass, and the work that needs it (#949) stopped. Repository policy bans accepting the old findings in a baseline. Judging the lines a change adds keeps the gate strict on new code and stops it failing on code the change never touched.

Verification

The commands below ran on the head of this pull request.

Criterion Command Result
#1073 AC1, AC2 go test ./cmd/shell-allowlist -run TestChangedLinesOnly passes
#1073 AC3 the same test, the continuation case passes
#1073 AC4 the same test, the -diff attribute case passes
#1073 AC4, text that resembles binary diff markers the same test, the marker-text case passes
#1073 AC5 go test ./cmd/shell-allowlist ./internal/shell passes
#1073 AC6 docs/shell-allowlist.md describes the scope present
Lint and tests mise run lint and mise run test-go pass

The nolint directives in cmd/shell-allowlist/changed.go, run.go and changed_test.go already exist on origin/main at changed.go:69, run.go:48 and changed_test.go:4, and this change adds none.

Doc impact: docs/shell-allowlist.md describes the new --changed scope.

Consumers receive the behavior with their next pinned shell-allowlist binary and don't change their configuration.

Risk

Loosens: yes

The changed mode no longer fails on findings that sit on lines a change doesn't add. A line that moves or gets reformatted counts as added, and a banned command added anywhere in a changed script still fails. --summary keeps counting the old findings until the Go ports retire the scripts. A diff header the parser can't read, fails the check rather than shrinking the judged lines. An edit to any line of a backslash-continued command word counts as touching it. Reverting the change restores whole-file judging.

Compatibility: none

Related

Closes #1073. Related to #588 and #949, which this change doesn't close.

…heck

The changed-script check parsed every touched shell file and reported
every unlisted command in it. Editing a long script that already ran
awk or python3 failed on lines the change never touched, so
no one could edit packages/agents/source/lib/lib.sh at all. The only
other option was accepting the old findings in a baseline, which
repository policy bans.

With a base ref, the command now reads the added line ranges from a
zero-context diff against the merge base and reports a finding only
when its line is one of them. A new script with no history still
counts in full, and the explicit path mode and the summary still scan
whole files. A banned command on an added or edited line fails as
before. Every line a change introduces gets the full check.

Loosens the gate: findings on unchanged lines stop failing the changed
mode. The summary command still counts them, which keeps the debt
visible until the ports retire it.

Assisted-by: claude:claude-sonnet-5-5
Signed-off-by: Tony Burns <tony@tonyburns.net>
@tbhb-agent tbhb-agent added change:fix PR fixes a defect (Conventional Commit type fix) provenance:agent-assisted PR contains commits authored with AI assistance (see Assisted-by trailers) labels Nov 2, 2026
@tbhb-agent
tbhb-agent requested a review from tbhb as a code owner November 2, 2026 14:39

@tbhb-dev-reviews tbhb-dev-reviews 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.

Reviewed head: 2e4f4e2
Review: CHANGES REQUESTED
Loosens: yes — existing findings are excluded in changed mode; the bypasses below exceed merely ignoring unchanged commands.
Owner-tier: no changed path matches either pattern. No constitutional-path approval requirement is triggered; the gate relaxation still needs the owner's exact-head approval.

Changed paths (all M; owner-tier / constitutional):

  • cmd/shell-allowlist/changed.go: no / no
  • cmd/shell-allowlist/changed_test.go: no / no
  • cmd/shell-allowlist/run.go: no / no
  • cmd/shell-allowlist/summary.go: no / no
  • docs/shell-allowlist.md: no / no

Policy patterns, quoted from .agents/agent-skills.conf:9 and .agents/agent-skills.conf:5:

constitutional_paths = ^(\.agents/(agent-skills\.conf|review-instructions\.md)$|\.github/(rulesets/|CODEOWNERS$)|\.claude/settings\.json$|packages/agents/source/(lib|skills/merge-pr/(shared|claude|codex)/scripts)/(merge-policy\.py|merge-agent\.sh|signing\.sh|lib\.sh)$|(packages/agents/[^/]+/\.apm/skills|\.(claude|agents)/skills)/[^/]+/scripts/(merge-policy\.py|merge-agent\.sh|signing\.sh|lib\.sh)$)
owner_paths = ^(\.agents/(agent-skills\.conf|review-instructions\.md)$|\.github/rulesets/|\.claude/settings\.json$|packages/agents/source/(lib/(lib|agent)\.sh$|skills/[^/]+/(shared|claude|codex)/scripts/guard[^/]*\.sh$|skills/merge-pr/(shared|claude|codex)/scripts/(merge-agent\.sh|merge-policy\.py)$)|(packages/agents/(claude|codex)/\.apm/skills|\.(claude|agents)/skills)/([^/]+/scripts/(guard[^/]*\.sh|(lib|agent)\.sh)$|(merge-pr|agent-merge-pr)/scripts/(merge-agent\.sh|merge-policy\.py)$))

AC assessment on this head:

Issue / live AC Met by this slice? Evidence / allocation
#588 AC1 No cmd/shell-allowlist/changed.go:83 changes gate scope; complex shell replacement remains in later slices.
#588 AC2 No, partial only cmd/shell-allowlist/changed_test.go:78 tests filtering, not migration parity. Later slices.
#588 AC3 No, not established cmd/shell-allowlist/changed.go:83 is a prerequisite change; ordered migrations remain later work.
#588 AC4 No, not established packages/agents/source/lib/lib.sh:575 retains review authority logic; this slice supplies no migration proof.
#588 AC5 No, not established cmd/shell-allowlist/run.go:16 implements no protected-write/check-reader migration. Later slices own that proof.
#588 AC6 No cmd/shell-allowlist/summary.go:35 changes a call site, not canonical generation or deployment. Later slices.
#588 AC7 No, not established cmd/shell-allowlist/run.go:22 changes behavior; replacement-binary deployment remains unproved here. Later slices.
#588 AC8 No, partial only All checks passed; .agents/agent-skills.conf:4 still does not protect this Go enforcement code. Epic completion is later work.
#949 AC1 No packages/agents/source/lib/lib.sh:508 still discards stderr; :669 names no diagnostic file. Deferred to #949.
#949 AC2 No packages/agents/source/lib/lib.sh:628 still names no stderr file. Deferred to #949.
#949 AC3 Yes, preserved packages/agents/source/lib/lib.sh:652 and :659 retain readable-verdict output without a stderr filename.

#949's latest Refinement verdict is READY; #588's comments contain status updates, without a Refinement verdict. Neither issue closes here. Related references are appropriate. Verification names tests but cites no AC IDs.

Findings:

  • P1 — cmd/shell-allowlist/run.go:69: filtering only the command word's starting line misses a newly banned continued word. A two-line command with line 1 consisting of r plus a trailing backslash and line 2 containing m is allowed rm. Change only line 2 to uby: the command becomes banned ruby, but its finding still points to unchanged line 1 and is discarded. Judge edits intersecting the complete command word and add a regression.
  • P1 — cmd/shell-allowlist/changed.go:97: diff output is not forced to raw text. A valid tracked script marked with a -diff attribute produces no hunks, so parseAdded returns an empty set and run.go:69 suppresses every finding, including an appended ruby command. Text conversion can likewise give unrelated line numbers. Force textual, non-text-converted input and refuse an unjudgeable changed script; add a regression.
  • P2 — cmd/shell-allowlist/changed.go:83: neither referenced issue supplies an AC authorizing this gate-policy change (#588 excludes policy changes as a side effect; #949 concerns stderr diagnostics). Link a scoped issue/AC contract for the relaxation and cite its exact-head evidence in Verification.

Boundary audit: ordinary added/edited lines and zero-count deletion ranges are covered at cmd/shell-allowlist/changed_test.go:108, :114, :120 and :134. Untracked files scan in full (:126); a rename's destination-only path diff conservatively scans it as an addition. No baseline file is introduced. Moving/re-adding a command is judged only if Git emits that physical line as added; moving surrounding lines can leave an old banned command matched as context. Whitespace edits normally count as additions, subject to the command-word flaw above.

Suppression audit: no new loosening markers, no t.Skip, and no weakened existing assertions. The requested zero-nolint condition is unmet: existing directives remain at cmd/shell-allowlist/changed.go:71, cmd/shell-allowlist/changed_test.go:4 and cmd/shell-allowlist/run.go:55. No unnecessary guard, option, abstraction or layer was found.

SIZE: scc 4.1.0 over added and deleted fragments gives 136 code / 2 prose lines, below 800 / 2000; no vendor changes. D4: all five changed files passed the private-string scan. Coverage: COMPLETED SUCCESS. CodeQL: COMPLETED SUCCESS, including Go and Actions analysis. All 23 reported check contexts are COMPLETED SUCCESS. Mergeability: MERGEABLE, merge state BLOCKED, not draft.
Round: 1 of 3

The first version of the added-lines filter matched a finding by the
line where its command word starts. A backslash can continue a word
over two or more lines. An edit to a continuation line could turn an
allowed command into a banned one. The finding still pointed at the
unchanged first line, and the filter dropped it.

Git also hides hunks for a file with a -diff attribute or a text
conversion filter, and an empty hunk set made the filter drop every
finding in that file.

A finding now records the last line of its command word, and any added
line inside that span counts. The check now diffs raw text, and a
script that still diffs as binary fails the check instead of passing
it unchecked.

Assisted-by: claude:claude-sonnet-5-5
Signed-off-by: Tony Burns <tony@tonyburns.net>
@tbhb-agent
tbhb-agent removed the request for review from tbhb November 2, 2026 15:51

@tbhb-dev-reviews tbhb-dev-reviews 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.

Reviewed head: 4ec38d0
Review: CHANGES REQUESTED
Loosens: yes — changed mode excludes unchanged command words, as #1073 permits. No new suppression markers.
Owner-tier: no changed path matches owner_paths or constitutional_paths. No constitutional-path approval is triggered. This gate relaxation still requires the owner's exact-head approval; none appears in the reviews.

Changed paths (git diff --name-status origin/main...HEAD; owner-tier / constitutional):

  • M cmd/shell-allowlist/changed.go: no / no
  • M cmd/shell-allowlist/changed_test.go: no / no
  • M cmd/shell-allowlist/run.go: no / no
  • M cmd/shell-allowlist/summary.go: no / no
  • M docs/shell-allowlist.md: no / no
  • M internal/shell/shell.go: no / no
  • M internal/shell/shell_test.go: no / no

Policy patterns (.agents/agent-skills.conf:9 and :5):

constitutional_paths = ^(\.agents/(agent-skills\.conf|review-instructions\.md)$|\.github/(rulesets/|CODEOWNERS$)|\.claude/settings\.json$|packages/agents/source/(lib|skills/merge-pr/(shared|claude|codex)/scripts)/(merge-policy\.py|merge-agent\.sh|signing\.sh|lib\.sh)$|(packages/agents/[^/]+/\.apm/skills|\.(claude|agents)/skills)/[^/]+/scripts/(merge-policy\.py|merge-agent\.sh|signing\.sh|lib\.sh)$)
owner_paths = ^(\.agents/(agent-skills\.conf|review-instructions\.md)$|\.github/rulesets/|\.claude/settings\.json$|packages/agents/source/(lib/(lib|agent)\.sh$|skills/[^/]+/(shared|claude|codex)/scripts/guard[^/]*\.sh$|skills/merge-pr/(shared|claude|codex)/scripts/(merge-agent\.sh|merge-policy\.py)$)|(packages/agents/(claude|codex)/\.apm/skills|\.(claude|agents)/skills)/([^/]+/scripts/(guard[^/]*\.sh|(lib|agent)\.sh)$|(merge-pr|agent-merge-pr)/scripts/(merge-agent\.sh|merge-policy\.py)$))
Live AC Met on reviewed head? Evidence / allocation
#1073 AC1 Yes cmd/shell-allowlist/changed.go:100 reads merge-base additions; cmd/shell-allowlist/run.go:69 filters findings; cmd/shell-allowlist/changed_test.go:105 covers unchanged, added and edited lines.
#1073 AC2 Yes cmd/shell-allowlist/changed.go:103 selects full-file judging for untracked files; cmd/shell-allowlist/changed_test.go:143 asserts it.
#1073 AC3 Yes internal/shell/shell.go:215 records the word's end; cmd/shell-allowlist/run.go:80 checks its entire line span; cmd/shell-allowlist/changed_test.go:129 is the continuation regression.
#1073 AC4 Yes cmd/shell-allowlist/changed.go:100 forces raw text without external diff or text conversion; cmd/shell-allowlist/run.go:61 fails on unparseable shell; cmd/shell-allowlist/changed_test.go:136 is the -diff regression. See the false-positive finding below.
#1073 AC5 Yes cmd/shell-allowlist/run.go:21 leaves explicit paths unfiltered; cmd/shell-allowlist/summary.go:35 passes nil; cmd/shell-allowlist/run_test.go:107 and cmd/shell-allowlist/summary_test.go:19 assert whole-file reports.
#1073 AC6 Yes docs/shell-allowlist.md:35 documents added-line scope and full judging of new scripts.
#588 AC1 No packages/agents/source/lib/lib.sh:504 retains complex shell; migrations belong to later slices.
#588 AC2 No cmd/shell-allowlist/changed_test.go:78 tests this prerequisite, not migration parity; later slices.
#588 AC3 No cmd/shell-allowlist/run.go:22 changes gate scope, without establishing the ordered migrations; later slices.
#588 AC4 No packages/agents/source/lib/lib.sh:652 retains authority/stamp behavior; migration proof belongs to later slices.
#588 AC5 No cmd/shell-allowlist/run.go:16 does not migrate protected writes or check readers; later slices.
#588 AC6 No cmd/shell-allowlist/summary.go:35 changes only a caller; generation/deployment migrations remain later work.
#588 AC7 No cmd/shell-allowlist/run.go:22 changes an existing binary; replacement-binary rollout remains later work.
#588 AC8 No Checks are green, but .agents/agent-skills.conf:4 does not protect this Go enforcement code; epic completion remains later work.
#949 AC1 No packages/agents/source/lib/lib.sh:508 discards stderr and :669 names no file; deferred to #949.
#949 AC2 No packages/agents/source/lib/lib.sh:628 names no stderr file; deferred to #949.
#949 AC3 Yes, preserved packages/agents/source/lib/lib.sh:652 and :659 retain readable-verdict output without a stderr filename.

The description cites #1073 AC1–AC6 with verification evidence and closes only #1073. #588 and #949 are Related and remain open. #1073 has no comments or Refinement verdict; #949's latest Refinement verdict is READY; #588 has status comments without a Refinement verdict.

Round-1 resolution: both P1 bypasses are closed with regression assertions at cmd/shell-allowlist/changed_test.go:129 and :136. The scoped #1073 contract and per-AC description evidence resolve the previous P2.

Findings:

  • P2 — cmd/shell-allowlist/changed.go:111: the binary guard searches the entire patch, including ordinary added/deleted source lines. Adding an allowed comment such as # GIT binary patch, or printf 'Binary files changed\n', makes a valid text script fail as binary. The same text in a deleted line also fails. This guard adds a false rejection beyond AC4. With --text already forcing textual output, remove the redundant guard, or recognize actual binary metadata outside hunk payloads. Add a regression for these valid text changes.
  • P2 — cmd/shell-allowlist/changed.go:72, cmd/shell-allowlist/changed_test.go:4, cmd/shell-allowlist/run.go:55: three pre-existing nolint directives remain in changed files, so the requested zero-nolint condition is still unmet. Resolve these sites to satisfy that condition; no new directive was introduced.

Boundary audit: zero-context new-side ranges include moved/re-added or whitespace-edited command lines when Git marks them added. A rename's destination-only diff conservatively judges the destination as a new file. Deleted files are excluded; zero-count deletion hunks add no judged lines (cmd/shell-allowlist/changed_test.go:123 and :153). Git may retain a relocated identical line as unchanged context, which AC1 intentionally excludes; this is not a new-command exemption. No baseline, allow-list entry or exemption file is added. No t.Skip or weakened assertion appears; EndLine assertions strengthen existing expectations. Apart from the binary guard finding, no unnecessary option, abstraction or layer was found.

SIZE: scc 4.1.0 on added plus deleted fragments against origin/main: 187 code / 2 prose lines, below 800 / 2000; 205 additions and 20 deletions physically; no vendor changes.
D4: all seven changed non-vendor files passed the private-string scan.
Checks: all 23 contexts COMPLETED SUCCESS. Coverage: COMPLETED SUCCESS. CodeQL: COMPLETED SUCCESS, including Go and Actions analysis.
Mergeability: UNKNOWN; merge state UNKNOWN; not draft (single status snapshot).
Round: 2 of 3

The changed-lines check searched the whole patch for the strings Git
prints for binary files. An ordinary added or deleted line that holds
the same text, such as a comment about a binary patch, made a valid
script fail the check.

The diff already runs with `--text`, so Git never emits binary markers
for these paths. Remove the redundant search and its error value, and
add a regression case for a script that contains both marker
strings.

Refs: #1073
Assisted-by: claude:claude-sonnet-5-5
Signed-off-by: Tony Burns <tony@tonyburns.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:fix PR fixes a defect (Conventional Commit type fix) provenance:agent-assisted PR contains commits authored with AI assistance (see Assisted-by trailers)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci(shell): judge only added lines in changed-script allow list check

1 participant