Repository navigation
fix(security): workflow/CI hardening batch (#1384, #1391, #1396, #1405) - #1419
Conversation
…titles (#1384) classify-initiative.sh printed raw issue/PR/draft titles to runner stdout and into ::warning:: lines. The runner treats the legacy `##[cmd]` marker anywhere in a line (and `::cmd::` at a line start) as a workflow command, so a title could forge annotations or stop command processing for the step. Add atp_log_safe to the shared add-to-project lib.sh: CR/LF become spaces and each `::` / `##[` gets a zero-width space between its characters, keeping the title readable while no longer matching either command syntax. Route every title the sweep logs through it, plus add_draft_item's dry-run line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1391) dl_dev_lead_active treated any open PR whose head branch started with dev-lead/issue-<n>- as proof the agent was working the issue. The open-PR list includes fork PRs, whose branch name the contributor chooses, so a fork PR could suppress compliance retriggers. Now a PR only counts when its head repo is the base repo (not a fork, not a deleted head repo) and its author is the dev-lead runtime identity (persona-standards.md 5.1: don-petry), overridable via DEV_LEAD_LOGINS (comma-separated, JSON-encoded by jq rather than spliced raw into the filter). Fail-closed behaviour on API errors is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmands (#1396) The auto-rebase reusable pasted the PR's base branch raw into the `git rebase origin/<base>` / `git merge origin/<base>` commands it publishes for maintainers to copy. Git branch names may contain shell syntax. Add auto_rebase_shell_quote to lib/comments.sh (plain refs stay unquoted for readability; anything else is single-quoted with ' -> '\'' , valid in bash, zsh and sh) and use it for both commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out (#1405) The scan ran `npx ecc-agentshield` with the PR checkout as cwd. npm loads project config from the .npmrc of its local prefix (the cwd), so the scanned PR's own .npmrc could set script-shell or registry for the scanner, e.g. make the scan step exit 0 without scanning. Run npx from an empty prefix under $RUNNER_TEMP (--prefix pinned to it) and pass the checkout as the absolute --path scan target. Version pin and --ignore-scripts are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Dev-Lead — review-changes (no-changes)No changes were needed for this PR. |
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-28T04:25:28Z. |
|
No description provided. |
|
This is a notice that the Codex review tool encountered an issue and could not complete the review run. The actual CI checks (linting, security scans, CodeQL, etc.) all passed successfully. This is an operational notice with no actionable code findings to address. |
Dev-Lead — fix-bot-comment (no-changes)Agent reasoning |
|
No description provided. |
|
This is a rate-limit notice indicating CodeRabbit has reached its free OSS review quota. There are no code findings to address—the bot is simply waiting for the quota to reset in 44 minutes. |
Dev-Lead — fix-bot-comment (no-changes)Agent reasoning |
Automated review — escalated to humanThe automated review cascade escalated this PR to a human reviewer at review cycle 0/3 (risk: HIGH, reviewed commit Why: the cascade could neither approve the PR nor auto-request fixes, so it requested human review via CODEOWNERS and set the Reviewer summary: Four security fixes, one commit each: neutralise runner-command markers in logged project titles (#1384); count only same-repo PRs by the dev-lead identity as dev-lead activity (#1391); shell-quote the base ref in auto-rebase recovery commands (#1396); run the AgentShield scanner's npx from a clean prefix outside the PR checkout (#1405). Each fix has regression tests. I found no defects. Triage cleared this as low-risk, but I am not confirming that. It changes trust boundaries: a login-based allow-list decides which PRs count as agent activity, and the org's security-scanner reusable now runs differently. Both are HIGH under the review taxonomy, so a human should sign off. CI is fully green. This note is updated in place on re-escalation; it is not re-posted. |
|
dev-lead is withholding action on this item. It is labeled To re-enable automated pickup: remove the |
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-28T12:47:42Z. |
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-28T16:41:10Z. |
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-30T02:37:45Z. |
GitHub logins are case-insensitive, so a DEV_LEAD_LOGINS entry whose casing differs from the API's missed the agent's active PR, and the caller could cycle its trigger label and start duplicate work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This comment has been minimized.
This comment has been minimized.
Addressed: logins compared case-insensitively via ascii_downcase; CodeRabbit confirmed in the thread.
|
@donpetry-bot please review |
|
@don-petry I'm on it — starting a fresh review now. Results will appear in a few minutes. |
|
@donpetry-bot please review |
|
@don-petry I'm on it — starting a fresh review now. Results will appear in a few minutes. |
donpetry-bot
left a comment
There was a problem hiding this comment.
Automated review — APPROVED ✓
Risk: MEDIUM
Reviewed commit: 674f483c00a7a756194c8a7e979db51a625d1728
Review mode: triage-approved (single reviewer)
Summary
Four security hardening fixes, one commit each, with regression tests: neutralise runner-command markers in logged project titles (#1384); count only same-repo PRs by the dev-lead identity as dev-lead activity (#1391); shell-quote the base ref in auto-rebase recovery commands (#1396); run AgentShield's npx from a clean prefix outside the PR checkout (#1405). The only change since the last cascade review (29d9d94) is a case-insensitive login comparison, which answers CodeRabbit's one finding. I found no defects and CI is fully green. I rate this MEDIUM, not HIGH (reasoning under Findings).
Linked issue analysis
- #1384: all five title log sites in
sweep_projectand theadd_draft_itemdry-run line now go throughatp_log_safe. It turns CR/LF into spaces and puts a ZWSP inside every##[and::. The loop handles:::/::::. Addressed. - #1391: the selector now requires a same-repo head (
.head.repo != nulland full_name equal to the base repo) and an author inDEV_LEAD_LOGINS(defaultdon-petry, compared case-insensitively). The logins are JSON-encoded byjq -Rbefore they go into the filter. It still fails closed on API errors. Addressed. - #1396:
MANUAL_BASE_REFis computed once withauto_rebase_shell_quoteand used in both thegit rebaseandgit mergesnippets. Addressed. - #1405: npx runs from
$RUNNER_TEMP/agentshield-npmwith--prefixand a minimalpackage.json, and scans the absolute checkout path.--ignore-scriptsand the version pin are kept. Addressed.
Findings
No blocking findings.
What I checked myself:
auto_rebase_shell_quote(bash 5.2):origin/maincomes back unchanged.origin/a'b,origin/x';touch PWNED;'y,origin/$(id)andorigin/&&`id`each come back intact througheval, and none of them ran a command.atp_log_safe:x ##[error]…, an embedded\n::error::,:::xand::::all come out with no remaining##[or::.- Logins filter: the
DEV_LEAD_LOGINSparser turns"don-petry, Foo ,,"into["don-petry","foo"]. A malformed value makes the jq filter fail, which falls into the existing fail-closed path (treated as active). That is the safe direction. - AgentShield coverage: this repo's caller uses the local reusable (ring0 dogfood), so the PR's CI ran the new step. Its output (
Files scanned: 2, grade A, 0 findings) matches the latestmainrun. The scanner covers the same files from the new prefix and has not gone quiet. - Review threads and AGENTS.md: the one CodeRabbit thread (case-sensitive logins) is resolved by 674f483 and CodeRabbit confirmed the fix. Decision logic stays in bats-tested scripts with the workflows as thin glue, as AGENTS.md requires.
Why MEDIUM, not HIGH: the earlier cascade escalated because of trust-boundary changes. On the content I disagree:
- No secrets, tokens, permissions, third-party actions or new attack surface are added. Each workflow change narrows an existing injection or config-hijack path.
- The login allow-list only decides whether to skip cycling a label. It controls no access, and it still fails closed.
- The scanner change has been checked against real CI output.
- Both reusable-workflow changes still reach callers only through the stable channel and canary rings.
Author's request to double-check #1391: a same-repo dev-lead/issue-<n>-* PR opened by anyone other than the dev-lead identity no longer suppresses the retrigger. This is intended and consistent with every existing dev-lead PR.
Non-blocking notes:
- Secret-scanning MCP tool is not available in this run. gitleaks CI passed.
- The
needs-human-reviewlabel from the earlier escalation is still on the PR. The owner has since asked for this re-review.
CI status
All required checks green: Shellcheck/bats, Lint, CodeQL (actions and python), SonarCloud (quality gate passed, 0 new issues), gitleaks, AgentShield, Agent Security Scan, dependency-audit and duplicate-decl-gate. CodeRabbit is NEUTRAL. The skipped jobs are the dev-lead and dependabot ones and package-manager audits that do not apply here.
Reviewed automatically by the PR-review agent (single-reviewer mode: opus 5.5 [opus 4.8, opus 4.7]). Reply if you need a human review.
|
pr-review approved on PARTIAL advisory evidence: 2/5 required advisory bots reported before the gate's head-age-timeout fallback proceeded. Recorded for the miss-rate metric (#1596). |
|
Maintainer disposition of A review-limit notice with no findings. CodeRabbit has since reviewed this PR, and its one inline finding (login case) was fixed in 674f483 and the thread resolved. |



Summary
Workflow/CI hardening batch for four CodeRabbit security findings. Each one was checked against the code and reproduced locally before fixing. Each fix comes with a regression test that fails on
origin/mainand passes on this branch. There is one commit per issue.classify-initiative.shlog linesscripts/lib/dev-lead-retrigger.shactivity checkauto-rebase-reusable.ymlmanual recovery commandsagent-shield-reusable.ymlscanner invocationFixes #1384: untrusted titles written raw to runner logs
How it was confirmed:
sweep_projectprinted«${title}»raw atclassify-initiative.sh:282, 288, 299, 303and also at 312 (the Theme warning). The runner parser (actions/runnerActionCommand.TryParse) matches the legacy##[prefix withIndexOf, so it fires anywhere in a line.TryParseV2(::cmd::) only fires at the start of a line, after leading whitespace is trimmed. A title such asx ##[error]…or##[stop-commands]…in anUNMATCHED/MATCHline was therefore read as a command. The new sweep tests reproduced it: the forged markers showed up verbatim in the output on main.Fix: added
atp_log_safeto the shared.github/scripts/add-to-project/lib.sh:::and##[gets a zero-width space (U+200B) between its characters, so the title stays readable but no longer matches either command syntax. The::replacement loops, so:::is handled too.All five title log sites in the sweep go through it. So does
add_draft_item's dry-run line, which is the only other title echo among the sibling scripts.add-issue-or-pr.shandreconcile-backlog.shonly log GitHub-generated URLs, repo names and config-defined labels.Regression tests (
test/workflows/add-to-project/classify-initiative.bats):atp_log_safe.sweep_projecttests with hostile titles: dry-runMATCH/UNMATCHED, and the apply-mode::warning::set-failure path. They check that no##[and no injected::line reaches the output.Fixes #1391: a fork PR branch name could fake dev-lead activity
How it was confirmed: at
dev-lead-retrigger.sh:39-47, thepulls?state=openfilter only checked.head.ref | startswith("dev-lead/issue-<n>-"). That list includes fork PRs, and fork authors pick their own branch names. A fixture fork PR ondev-lead/issue-7-…madedl_dev_lead_activereturn "active" on main.Fix: a PR now counts only when all of these hold:
.head.repo != nulland.head.repo.full_name == .base.repo.full_name, i.e. the PR comes from this repo, not a fork or a deleted head repo;.user.loginis the dev-lead runtime identity.That identity is
don-petry, as documented instandards/persona-standards.md§5.1. All 154dev-lead/PRs in this repo were authored by that login. It can be overridden withDEV_LEAD_LOGINS(comma-separated). The logins are JSON-encoded withjq -R, not spliced into the filter as raw strings. Fail-closed behaviour on API errors is unchanged. I also ran a read-only smoke test of the new--jqfilter against realgh/gojq on this repo: open PR #1383 (dev-lead/issue-1361-…) is still detected as active.Regression tests (new
test/scripts/compliance-retrigger/dev-lead-active.bats, 10 tests). Theghstub applies the real--jqfilter to fixtures. Cases covered:head.repo: nullDEV_LEAD_LOGINSoverride and injection attemptThe suite is already run by
compliance-retrigger-tests.yml, which runs thetest/scripts/compliance-retrigger/directory onscripts/lib/**changes.Fixes #1396: base branch pasted unescaped into published shell commands
How it was confirmed:
auto-rebase-reusable.yml:218(git rebase origin/$BASE_BRANCH) and:258(git merge origin/$BASE_BRANCH) interpolate.base.refraw into the copy-paste snippets in PR comments.git check-ref-format --branchaccepts names containing;,$(…), backticks,|,&&and quotes. The tests below assert that precondition for each case.Fix: added
auto_rebase_shell_quoteto.github/scripts/auto-rebase/lib/comments.sh, which the reusable already sources from its own commit.[A-Za-z0-9_./+-]are printed unchanged, soorigin/mainstays readable.'written as'\''. That form is valid in bash, zsh and sh, unlike%q.The reusable computes
MANUAL_BASE_REFonce and uses it in both snippets. The README function table is updated.Regression tests (new
test/workflows/auto-rebase/manual-command-quoting.bats):evalandsh.origin/$BASE_BRANCHinto a git command, and that both snippets use the quoted ref.Fixes #1405: the PR's
.npmrccould configure the AgentShield scannerHow it was confirmed:
agent-shield-reusable.yml:60runsnpx --ignore-scripts ecc-agentshield@… scan --path .with the PR checkout as cwd. npm loads project config from<localPrefix>/.npmrc, and localPrefix is cwd-based. I reproduced this locally with npm 11.11.0. In a directory whose.npmrcsetsscript-shell=/usr/bin/true,npx --ignore-scripts -c 'exit 3'exits 0, so the command is silently skipped. Run from a clean prefix, the same command exits 3.npm config get registrysimilarly showed the hostile registry from inside the directory.Fix: the scan step now:
$RUNNER_TEMP/agentshield-npmcontaining a minimalpackage.json, which pins localPrefix;cds there;npx --prefix=<that dir> --ignore-scripts ecc-agentshield@${AS_VERSION} scan --path "<absolute checkout path>".The version pin and
--ignore-scriptsare unchanged. I checked in theecc-agentshieldv1.4.0 source thatscan --pathresolves its argument, so an absolute target works. The change is limited to the scan step, so it does not conflict with #1378, which edits the inputs and the cross-reference steps of this file.Regression test (
test/workflows/dependency-hardening.bats, which already runs on every.github/workflows/**change): it extracts the step's realrun:script withyqand executes it inside a fake checkout that has a hostile.npmrc, with a stubnpxthat records its cwd and args. It asserts:--prefixequals that cwd;--pathis the checkout;--ignore-scriptsis kept;npm config get script-shellwith the recorded flags does not resolve the hostile value. A precondition first confirms the fixture is hostile when npm runs inside the checkout.Tests run (locally, bats 1.13 with GNU bash 5)
bats test/workflows/add-to-project/: 88/88 passbats test/scripts/compliance-retrigger/: 22/22 passbats test/workflows/auto-rebase/: 52/52 passbats test/workflows/dependency-hardening.bats: 5/5 passbats test/scripts/compliance-audit/(it also sourcesdev-lead-retrigger.sh): 298/298 passorigin/maintree fail as expected.shellcheck -xon the changed scripts: clean apart from one existing SC2016 info inlib.sh(delete_project_item's GraphQL string, not touched here; CI runs at-S warning).actionlinton both changed workflows: only an existinggithub.job_workflow_shafalse positive from my local actionlint version, on a line not touched here.gitleaks detect --no-git: no leaks.Release notes / follow-ups
auto-rebase/stableandagent-shield/stable, after the release cut and canary rings. No tags were moved in this PR.add-to-project-reconcile.yml) and dev-lead-retrigger.sh trusts the dev-lead/ branch prefix as proof of agent activity; fork PRs can suppress compliance retriggers #1391 (compliance-retrigger.yml/ compliance audit) run from this repo'smainand take effect on merge.DEV_LEAD_LOGINSenv (defaultdon-petry). If the dev-lead persona'sruntime.identity.accountever changes, set this or update the default.auto-rebase-reusable.ymlalso usesBASE_BRANCHas an associative-array key and in API paths. Neither is a copy-paste shell command, so both are outside auto-rebase-reusable: PR base branch name is pasted unescaped into the published recovery shell command #1396.Needs a human decision
dev-lead/issue-<n>-*PR opened by anyone other thandon-petryno longer suppresses the retrigger. That matches every existing dev-lead PR in this repo.🤖 Generated with Claude Code
Problem
See the linked issue.
Risk
Medium — changes GitHub Actions workflow behavior, which is exercised only post-merge; verify via the affected workflow runs.
Test plan
Tests added/updated:
test/scripts/compliance-retrigger/dev-lead-active.bats,test/workflows/add-to-project/classify-initiative.batstest/workflows/auto-rebase/manual-command-quoting.bats,test/workflows/dependency-hardening.bats. Verification:bash scripts/dev-lead-lint.sh(shellcheck --severity=warning) ran pre-commit; the bats suite runs in CI.Rollback
Revert this PR. No non-revertible side effects (no tags, migrations, or external state).
Monitoring
Watch the affected workflow run(s) in the Actions tab and this PR's Lint check for regressions.