Add per-repo autocommit opt-in for /close archiving#49
Open
gering wants to merge 3 commits into
Open
Conversation
Route step 10's commit+push prompt straight to archive-task.sh commit-push when a repo commits .claude/work-system-close-autocommit, skipping the y/n ask in repos where archiving-to-main is the norm. Off by default; the flag is an authorization scoped to exactly that one narrow, already-guarded action (pathspec commit, ff-only, never force-pushes). - archive-task.sh: new `autocommit get|set|unset` subcommand - close/SKILL.md: check the flag before asking, same result reporting either way - test_archive_task.py, docs, CHANGELOG, work-system 1.9.4 -> 1.10.0
Three review rounds against the initial cut found the flag's trust model too weak and the write path unsafe. Presence no longer authorizes anything: the value is read from the committed object on <main-branch>, so neither a file a tool merely wrote nor a working-tree edit hidden by --assume-unchanged/--skip-worktree can waive /close's push prompt, and the verdict no longer depends on which branch the checkout sits on. - get: read from <main-branch>:<rel>; bare enabled=no when nothing is configured, else reason= + a ready-to-print note= (callers relay it instead of restating the vocabulary); locally-disabled is reported neutrally rather than told to revert itself - set/unset: delegate resolution to main-repo-path.sh and cross-check it is a real work tree; refuse a symlinked or wrong-typed flag/.claude and re-validate after mkdir; mktemp+rename instead of a PID-derived temp path; warn when .claude/ is gitignored; remind to commit the deletion - commit-push: :(literal) pathspecs so a task named `x*` cannot sweep in other archives (not for check-ignore, which rejects pathspec magic); archive() uses mktemp too - close/SKILL.md: pass <main-branch>, show the scoped status preview on the auto path, fail closed on any non-enabled=yes output - docs: state what the guard is and is NOT worth (it raises the bar to "a commit", not "human-reviewed"); CLAUDE.md marker -> v1.10.x Scope stated honestly in the knowledge entry: agent-registry.sh still open-codes the old resolver, and separate-git-dir layouts are refused rather than supported.
Round 4 of the swarm review found the authorization ref was read by bare name, so a tag named like the default branch shadowed refs/heads (git resolves refs/tags first, and tags auto-follow on fetch) — letting a value the branch never carried authorize an unattended commit+push. Verified, and now covered by a regression test. - get: read refs/heads/<main-branch>, fully qualified; fail CLOSED when the branch is unresolvable instead of falling back to HEAD; decline when the checkout is not on that branch (commit-push would refuse anyway, so enabled=yes only announced a commit that then bailed) - unset: take the same optional <main-branch> so the "commit this deletion" reminder uses the ref that actually grants authorization - one autocommit_normalize() for both the committed and working-tree values; they had drifted (only one rejected multi-line content) - archive: verify a real regular file is in place before removing the source; derive the mode from umask rather than forcing 644; use the shared lit() helper for its committable probe - lit() hoisted to top level, with the actual rule stated: prefix every command that interprets pathspec magic, omit for check-ignore, which rejects it - close/SKILL.md: authorize on a whole-line `enabled=yes` match, never a substring — note= can carry repo-controlled text - header/usage/CHANGELOG/knowledge corrected to match the code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/closestep 10's "commit the archived task file tomainand push? [y/n]" prompt is pure friction in repos where archiving-to-main is the established norm (dotfiles, this repo). This adds a per-repo opt-in that skips it..claude/work-system-close-autocommitflag routes/closestraight toarchive-task.sh commit-push— noAskUserQuestion— while still printing the same scopedgit statuspreview first. Standing authorization is not authorization to act unseen.Trust model (the part worth reviewing)
Presence does not authorize anything.
getreads the value from the committed object onrefs/heads/<main-branch>:refs/tags/beforerefs/heads/— a tag named like the default branch (tags auto-follow on fetch) otherwise supplied a value the branch never carried.git update-index --assume-unchanged/--skip-worktreecannot hide an edit that a diff-based guard would call clean..claude, unsupported layout, unreadable git state →enabled=no, which merely restores the prompt.Stated honestly: this raises the bar from "anything can write a file" to "something had to commit" — not to "a human reviewed it". Whoever can commit in your repo can commit this too. What bounds the damage is
commit-pushitself: archive-scoped pathspec, fast-forward only, never a force-push, refused whenmaincarries other unpushed commits. Worst case is inert archived markdown reachingmainunreviewed.Changes
archive-task.sh— newautocommit get|set|unsetsubcommand as the single source of truth for the flag. Resolution delegates tomain-repo-path.sh(the same resolver/closeuses, sosetcannot write wheregetwon't look) and is cross-checked as a real work tree.set/unsetrefuse a symlinked or wrong-typed flag or.claudeparent, re-validate aftermkdir, and write via an exclusively-createdmktemp+ rename.commit-pushpasses:(literal)pathspecs so a task namedx*cannot sweep in other archives.close/SKILL.md— passes<main-branch>, shows the preview on the auto path, and authorizes only on a whole-lineenabled=yesmatch (never a substring —note=can carry repo-controlled text). Any other output, including none, falls through to asking.test_archive_task.py— new suite against real git repos, with regression tests for each reproduced bypass: tag shadowing, the index bits, the symlinked.claudeparent, relative-path helper resolution, glob-y task names, gitignored.claude/.CLAUDE.mdversion marker.plugin.json+marketplace.jsonin sync.Review provenance
Five
/swarm:review --looprounds (claude + codex + grok), 52 findings, 51 applied. Four criticals were self-inflicted — each round's hardening created the next round's bug: theassume-unchangedbypass,$(dirname $0)resolving inside acd'd subshell (ran scripts from the target repo),:(literal)passed tocheck-ignore(broke gitignore detection), and the tag-shadowing above. All reproduced, fixed, and pinned by tests.Deliberately not done, with reasons in the knowledge entry: requiring the flag's commit to be an ancestor of
origin/<default>(breaks origin-less repos, disproportionate to the blast radius), and migratingagent-registry.shto the shared resolver (sits on the/kickoffhot path — follow-up).Known residuals:
separate-git-dirlayouts are refused rather than supported (pre-existing, belongs inmain-repo-path.sh); the write-path TOCTOU window is narrowed, not closed (needsopenat/O_NOFOLLOW, unavailable in shell).Readiness
check-structure.py+ plugin tests: 0 errorsTest plan
mainand the main checkout onmain,/closeshows the preview, commits+pushes without prompting, and reports thecommit-pushresult./closeasks exactly once (unchanged).set→getreportsuntrackeduntil committed; a tag named like the default branch does not enable it; a local edit tonoreportslocally-disabled.🤖 Generated with Claude Code