Audit: CI review automation and developer automation¶
What we check¶
Automated review¶
- Claude Code automated review runs in the CI workflow, only after all other tests pass.
- The reviewer job must be a call to the shared reusable workflow
shakenfist/actions/.github/workflows/pr-auto-review.yml@main, with the project's own test jobs in itsneeds:list. Writing the reviewer job out in full in the project's CI workflow is superseded: projects still carrying a hand-writtenautomated_reviewerjob should migrate to the reusable workflow and delete theircheck-bot-commitjob, which the reusable workflow replaces with an API call. - The reviewer must reach Claude Code through the shared action
shakenfist/actions/review-pr-with-claude@main(not per-project scripts). The reusable workflow does this for its callers. - The calling job needs
pull-requests: writeandissues: writepermissions, because a cross-repository reusable workflow cannot grant itself more token scope than its caller has. - The calling job must not pass
secrets: inherit.pr-auto-review.ymldeclares no secrets and reads none -- it andreview-pr-with-claudeauthenticate withgithub.token, which comes from thepermissions:block above -- so inheriting buys the caller nothing while handing every secret the repository holds, publishing tokens included, to a workflow in another repository. The exposure is latent rather than active, but it means a bad change landing inshakenfist/actionswould already have those secrets within reach. This applies to thepr-auto-review.ymlcall only: callers ofsmoke-cluster.ymlandexport-repo-config.ymldo read secrets and inherit correctly. - The automatic review must not pass
forceto the review action, so that a PR the bot has already reviewed is left alone.pr-re-review.ymlis the only workflow which setsforce, making an explicit human request the sole way to override an existing review. - The reviewer runs Claude Code with
--dangerously-skip-permissionswhile holding a write-capable token, and the PR diff is untrusted input, so the automatic review must be restricted to same-repository pull requests. Fork PRs are reviewed only on explicit human request.
Developer automation¶
Projects should include bot-triggered workflows responding to
@shakenfist-bot comments from authorised users:
pr-re-review.yml-- triggers another automated review (withpull-requests: writeandissues: write).pr-retest.yml-- re-runs functional tests.
The comment addresser is retired¶
pr-address-comments.yml answered @shakenfist-bot please address
comments by handing the review's items to Claude Code and pushing a
commit per item. It was retired in August 2026 because it went unused:
review items are worked through interactively with the reviewer
instead, and a bot authoring commits from a review no human had read
was the part that stopped anyone reaching for it.
Its remains are audited rather than ignored, because they are not
inert. The workflow triggers on issue_comment, so it holds
contents: write against the pull request branch for a feature nobody
wants; and it is the last thing in a project that calls
render-review.py, so the script and its schema are dead weight the
next project copies. The check therefore looks for the whole chain:
.github/workflows/pr-address-comments.ymladdress-comments-with-claude.shrender-review.pyreview-schema.json
All four are searched for by basename anywhere in the tree. tools/
and .github/workflows/ are the canonical homes, but deployments put
them elsewhere -- the check this replaced found a contrib/ copy, and
a template directory carries the copy of the workflow the next project
installs -- and a dead file is dead wherever it sits. Naming only the
installed workflow would mean a maintainer who removes everything the
finding names deletes the scripts, leaves the template copy behind, and
passes the audit from then on while still handing the chain to the next
project.
Only a copy at .github/workflows/pr-address-comments.yml actually
runs, so only that one holds contents: write on the pull request
branch. The finding says so when it is present and calls the rest dead
weight when it is not, rather than asserting a privileged workflow the
maintainer would then go looking for and not find.
One exemption. A directory holding an action.yml or action.yaml is
a composite action's own source rather than a deployed copy, and is
skipped.
shakenfist/actions is in the matrix and is where
review-pr-with-claude/render-review.py and its schema actually live
-- the copies every project's reviewer runs, and the ones this
retirement sends projects to instead of their own. Without the
exemption the finding would name them, and the instruction below is to
remove everything it names in one commit, which would delete the
renderer out from under the reviewer in every repository at once. The
exemption covers the directory the manifest sits in and nothing below
it, and shakenfist/actions is still reported for the leftovers it
genuinely carries elsewhere in its tree.
Everything the finding names goes in one commit. Deleting the workflow
and keeping the scripts leaves the copy that gets propagated. The
reviewer is otherwise unaffected: it reaches render-review.py
through shakenfist/actions/review-pr-with-claude@main, which carries
its own copy and its own schema.
The trigger handling must be the shared action¶
pr-re-review.yml must reach shakenfist/actions/pr-bot-trigger@main
rather than hand-rolling the phrase match, permission lookup, reaction
and refusal reply in inline shell. pr-retest.yml already does.
This is a security requirement, not a tidiness one. pr-bot-trigger
refuses pull requests from forks, and a hand-rolled copy does not
inherit that. The action's pr-ref output is .head.ref -- the branch
name in the head repository, carrying nothing to say which repository
that is -- and callers hand it to actions/checkout and to
git push origin HEAD:refs/heads/<ref> against their own
repository. Fork pull requests are commonly opened from the fork's
default branch, so .head.ref is literally main: the checkout
succeeds against the target's main, the bot commits to it, and the
push lands unreviewed commits there. No malice is required -- a
maintainer typing the trigger phrase on a fork pull request is enough.
Because the guard lives in the action, every workflow that uses it
picked the fix up at @main with no change on its side. That is the
whole argument for the requirement: a shared action is how a fix
reaches ten repositories at once, and a local copy is how one of them
misses it.
An earlier version of the template open-coded this, which is why every
deployment needs replacing rather than editing. The template copy had
also drifted in ways that matter less but point the same way: it
reacted with +1 instead of rocket, worded its refusal differently,
and never checked the trigger phrase itself, so it could not distinguish
"phrase not matched" from "not authorized".
The check reports nothing when pr-re-review.yml is absent -- that is
already a finding on its own, and reporting both would be two findings
for one missing file.
Test drift fixing (optional)¶
Projects with large test suites prone to drift should also add:
pr-fix-tests.yml+test-drift-fix.yml-- triggers Claude Code to fix CI failures.
These use shared composite actions from the actions/ repository:
shakenfist/actions/pr-bot-trigger@mainshakenfist/actions/review-pr-with-claude@main
Automated reviewer prompt¶
The automated reviewer's prompt should ensure it checks that
documentation in the docs/ directory has been updated for any
user-visible changes.
Template¶
Template: templates/ci-review-automation/
See: templates/ci-review-automation/README.md
Docs: docs/ci-review-automation.md, docs/automated-pr-review.md
Test drift fixing template: templates/test-drift-fix/
See: templates/test-drift-fix/README.md
Projects¶
This table is regenerated daily by the consistency audit
workflow from scripts/audit-check.py results; do not edit
it by hand.
Last regenerated: 2026-08-23T06:45:38.740880+00:00
| Project | Status | Issue |
|---|---|---|
| actions | non-compliant | actions#36 |
| agent-python | non-compliant | agent-python#126 |
| client-python | non-compliant | client-python#367 |
| client-python-k3s | non-compliant | client-python-k3s#36 |
| clingwrap | non-compliant | clingwrap#121 |
| cloudgood | non-compliant | cloudgood#1 |
| development | compliant | - |
| divergulent | non-compliant | divergulent#36 |
| instar | non-compliant | instar#515 |
| kerbside | non-compliant | kerbside#360 |
| kerbside-patches | non-compliant | kerbside-patches#949 |
| library-utilities | non-compliant | library-utilities#32 |
| occystrap | non-compliant | occystrap#120 |
| private-ci | N/A | - |
| ryll | non-compliant | ryll#303 |
| sfui | non-compliant | sfui#26 |
| shakenfist | non-compliant | shakenfist#3314 |
Details for non-compliant projects:
- actions (Status): the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh); it is unused, and its workflow holds contents: write on the pull request branch
- agent-python (Status): pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml); it is unused, and its workflow holds contents: write on the pull request branch
- client-python (Status): pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml); it is unused, and its workflow holds contents: write on the pull request branch
- client-python-k3s (Status): pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh); it is unused, and its workflow holds contents: write on the pull request branch
- clingwrap (Status): pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh, tools/render-review.py, tools/review-schema.json); it is unused, and its workflow holds contents: write on the pull request branch
- cloudgood (Status): Missing workflows: pr-re-review.yml
- divergulent (Status): Missing pr-re-review.yml; Missing pr-retest.yml; No workflow uses shared action review-pr-with-claude@main
- instar (Status): pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh, tools/render-review.py, tools/review-schema.json); it is unused, and its workflow holds contents: write on the pull request branch
- kerbside (Status): the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh, tools/render-review.py, tools/review-schema.json); it is unused, and its workflow holds contents: write on the pull request branch
- kerbside-patches (Status): Missing pr-re-review.yml; Missing pr-retest.yml; No workflow uses shared action review-pr-with-claude@main
- library-utilities (Status): Missing pr-re-review.yml; Missing pr-retest.yml; No workflow uses shared action review-pr-with-claude@main
- occystrap (Status): pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh, tools/render-review.py, tools/review-schema.json); it is unused, and its workflow holds contents: write on the pull request branch
- ryll (Status): the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh, tools/render-review.py, tools/review-schema.json); it is unused, and its workflow holds contents: write on the pull request branch
- sfui (Status): pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh, tools/render-review.py); it is unused, and its workflow holds contents: write on the pull request branch
- shakenfist (Status): Missing pr-retest.yml; pr-re-review.yml does not use shakenfist/actions/pr-bot-trigger@main, so it hand-rolls the trigger handling and does not inherit the action's fork pull request guard; the retired comment addresser is still deployed (.github/workflows/pr-address-comments.yml, tools/address-comments-with-claude.sh, tools/render-review.py, tools/review-schema.json); it is unused, and its workflow holds contents: write on the pull request branch