Repository navigation
fix: make an image push fail, and remove the pages workflow - #36
Conversation
The harness image holds harnesses that the project may not redistribute, and it is built from unreleased source. Thus nothing may publish it. A repository check in make check, a guard rule and the workflow permission now each stop a push. The pages workflow could publish unreleased pages, so it is removed. The old repository name is also removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2
…look The adversarial pass got many pushes past both breakers. Examples: an engine in a variable, a continued line, quoted words, docker build --push, crane cp, and Rust and Python that pass "push" as an argument. The check also skipped files with odd names and passed on a grep error. Both breakers now read the command as a shell does. They cover more tools and verbs, and GitHub releases. The check fails when git or awk fails. Each new rule has a test that fails when the rule is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2
The pass found three faults that this branch made: - In a linked worktree's pre-commit hook, the test acted on the real repository. The test now unsets the git variables, and it stages nothing. - The guard looped forever on a bare env, sudo or xargs. - The guard recursed on bash -o pipefail -c. Both breakers now pass over the options of the words in front of a command, and over the values of an engine's options. They refuse compose -f pushes, release and login actions, and bake files. They no longer refuse imagetools inspect, a pull with skopeo, or --debug logs push. Each guard case has a time limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2
The pass found these problems: - The guard joined a line that ends in an escaped backslash, which hid a git add -A on the next line. - Both breakers read any variable in the command position as an engine, and they read the options of a container's own program. - They refused cache pulls, local copies and builds, and secrets actions. Both breakers now count a variable as an engine only when its name says so. They stop at run and exec, and they skip option values. The guard strips the quotes once for each command, not once for each word. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bhj4Ck4y5iwZUsjpgQx8A2
There was a problem hiding this comment.
🔵 Needs a closer look
The change introduces a large, parsing-heavy security scanner with mirrored guards and workflow permission changes whose many bypass edge cases and policy completeness warrant human sign-off.
No open issues 🎉
What changed in this PR
This PR hardens the repository against ever publishing the harness image, which bundles vendor‑licensed CLIs (Claude Code, Copilot CLI) the project may not redistribute and is built from unreleased source. It adds a file scanner and a session-command guard that refuse any image push/registry login/release, documents the policy, removes the GitHub Pages workflow (which could publish unreleased pages), and completes the repository rename from sstriker/durable-memory to apache/tooling-memory.
Changes:
- New
scripts/no-image-push.sh(+ 218-case test), wired intomake check, that reads every tracked/added file as a shell would and fails on image pushes, registry logins, releases,packages: write, and bare"push"strings in Rust/Python/JS. - Extended
.claude/hooks/guard-bash.shto refuse the same commands when typed (with improvedsudo/env/xargs/timeoutprefix handling,bash -…cunwrapping, and odd-backslash line joins), plusgh release create/upload/edit. - Removed
.github/workflows/pages.yml, addedpackages: nonetoimage.yml, and updated docs/metadata to the newapache/tooling-memoryname (internaldurable-memoryproject/image name retained intentionally).
| File | Description |
|---|---|
scripts/no-image-push.sh |
New awk+sh scanner refusing image push/login/release paths across the repo. |
scripts/no-image-push.test.sh |
Comprehensive test suite (refusals, allowances, whole-repo/failure modes). |
.claude/hooks/guard-bash.sh |
New check_image_push, release-command refusal, and refined prefix/-c parsing. |
.claude/hooks/guard-bash.test.sh |
Adds refuse/allow cases and a per-case timeout to catch guard loops. |
Makefile |
Adds no-image-push-test target and wires it into check. |
.github/workflows/image.yml |
Documents the no-publish rationale; declares packages: none. |
.github/workflows/pages.yml |
Removed; it could publish unreleased pages to GitHub Pages. |
images/harness/README.md |
Adds the "Never published" policy and what the guards do/don't stop. |
AGENTS.md |
Adds the "No image leaves this machine" non-negotiable and updates the guard summary. |
local/README.md |
Rewrites hosted-pages note to state the repo does not publish. |
images/harness/Dockerfile |
Updates image source label to the new repo URL. |
Cargo.toml, CONTRIBUTING.md |
Update repository URL / clone instructions to apache/tooling-memory. |
crates/memory/src/open.rs |
Updates test URL fixtures to the new name (behavior unchanged). |
No blocking defects were found. I verified the scanner and guard produce correct accept/reject results on representative cases (e.g. docker compose -f … push, $ENGINE push, gh release create refused; skopeo copy … oci:node, ko build -L, $CARGO login, gh release download allowed), the rename is complete where intended (remaining @sstriker is the maintainer handle; spikes/ and adversarial.md are intentionally deferred), and no documentation still references the removed Pages workflow.
Review effort: Balanced
Summary
Nothing in this repository publishes the harness image, and nothing may. This change writes down why, and adds checks so that a push does not happen by accident. It also removes the workflow that could publish the web pages, and it removes the old repository name.
Why the image must stay local:
This change does these things:
scripts/no-image-push.sh, run bymake check.sudo -u root,env -i,xargs -n 1,timeout 600,then,sh -c, a YAMLrun:);docker -H host push,compose -f a.yml push);"$runtime" push,$(ENGINE) push).--push,push=trueandtype=registry;gh release create,uploadandedit;packages: write,pages: writeandwrite-all;"push"..claude/hooks/guard-bash.shrefuses the same commands when a session types them..github/workflows/image.ymlnow namespackages: none. With other permissions named, GitHub already gives nopackagespermission, so this line only makes the rule visible..github/workflows/pages.ymlcould publish the web pages, built frommain, to GitHub Pages. That is unreleased material outside the development community. Nobody ran it.make hostedandamem --hostedstay, for a person who hosts a copy for their own use.Cargo.toml,CONTRIBUTING.md, the image label, the guard tests and a test incrates/memory/src/open.rs. The records inspikes/keep it.images/harness/README.mdand AGENTS.md give the rule and the reasons. The README also says what the checks do not stop. They stop an accident, and they are not a security boundary. A workflow that a branch changes runs beforemake checkcan stop it. Only a setting of the repository or of the organisation can stop that first run, so ASF Infra can turn off GitHub Packages for this repository if that matters.The guard change also closes older gaps in the git rules. These were allowed on
mainand are now refused:then git add -A,sudo -u me git add -A,bash -lc 'git add -A', and a baretimeoutno longer hangs.Third-party GitHub Actions
I compared each action in the workflows with the ASF list in
apache/infrastructure-actions(actions.ymlandapproved_patterns.yml):actions/checkout,actions/setup-node,actions/upload-artifactactions/*is allowed with no entrydtolnay/rust-toolchain4cda84d5…4cda84d5…onlySwatinem/rust-cachetaiki-e/install-actionAll are allowed.
dtolnay/rust-toolchainis allowed at one commit only, so a bump of that pin needs a change to the ASF list first.Tests
scripts/no-image-push.test.shhas 218 cases:.claude/hooks/guard-bash.test.shhas 94 new refusals and 60 new allowed commands. Each guard case now has a time limit of ten seconds, so a guard that loops fails the test.The new refusals failed against the guard on
main:The guard reads the text of each command and runs none of them.
That was the first 16 cases. For each rule in both breakers, I made the rule wrong in a scratch copy, and a test failed each time. Examples:
$runtimeengine: 9;sudo,xargs,nicewith no option values: 1 each;run: 1;env: 5 cases failed on the time limit.make checkpasses: 718 tests run, 718 passed.Adversarial pass
Three fresh agents attacked the branch, one after each revision.
docker build --push,docker compose push,crane cp, and Rust and Python that pass"push"as an argument. It also found that the check skipped files with odd names, and passed on a grep error. The second commit answers these.core.bare=true. The test now unsets the git variables and stages nothing. I ran the test with the variables that such a hook sets, in a scratch clone with a linked worktree. The index and the shared config did not change.env,sudoorxargs.bash -o pipefail -c.It also found more ways past the checks, for example
compose -f a.yml pushand release actions. The third commit answers these.bash --posix, mawk or busybox. It replayed 2,171 of the repository's commands through the guard, and there were no differences frommain. It found a join of a line that ends in an escaped backslash, which hid agit add -A. It also found some refusals in error:imagetools inspect, a pull withskopeo,ko build -L, and a variable that names no engine. The fourth commit answers these.These stay open:
grep -Idoes not skip binary files. Thus an untracked tarball is read, and one test case cannot fail there..claude/commands/adversarial.mdstill names the old repository. The reader-test PR changes that line.Posted by Claude on behalf of @sstriker