Post #3 in the Claude Code Toolkit series. Post #1 was nf-agents, about spawning teams without forgetting flags. Post #2 was nf-dream, about cleaning up the memory swamp those teams leave behind. This one is about a smaller, sharper problem: I want Claude to commit, push, and merge without asking permission every single time, but only inside repos where I have decided that is fine.
The friction the skill solves
The global safety rule for my Claude Code setup says roughly: never run git commit unless the user explicitly says “commit”, never run git push unless the user explicitly says “push”, words like “ok” or “done” or “sync” are not confirmations. The rule is correct for client repos and any codebase where a stray commit would be awkward.
It is wrong for this blog. On a typical week night I write three or four posts. Each cycles through type-the-draft, fix-the-typo, run-the-em-dash-grep, fix-the-matches, commit, push, deploy. Twenty round-trips per evening, and every one ends with me typing “commit” into a prompt before anything moves. The friction is not the typing. The friction is that the work has a natural rhythm (write, check, ship) and the confirmation step interrupts it for no payoff. I own the repo. I write all the commits. I am the only one who reads the diff.
The obvious fix is to delete the global rule. But the global rule is correct everywhere else. The day after I delete it, I open a client repo, fix a typo in a config file, and the agent silently pushes a commit to a branch a coworker is reviewing. I want the rule to keep applying everywhere except where I have opted out.
nf-git-workflow is the per-repo opt-out.
What the skill is
It is a generator. You run /nf-git-workflow inside a repo, pick one or more authorization modes, and the skill writes a file at .claude/rules/git-workflow.md (or appends a marker block to CLAUDE.md). The next Claude Code session loads that file at startup. From then on, in that repo only, the agent moves quickly. Everywhere else, the global rule still applies.
The skill itself does not do anything at runtime. It is a one-shot installer. Once the rule is written, Claude Code does the rest by loading the file and treating it as authoritative for the current repo. Re-running the skill replaces the previous install via marker comments, so iterating on the config is cheap.
The three modes
The thing the skill is configuring is not “yes commit / no commit.” It is how work lands on the protected head branch. Three options, ordered from safest to fastest:
| Mode | How work lands | Use when |
|---|---|---|
worktree-pr | Agent works in worktree/<slug>, opens a PR with gh pr create --base <head>, auto-merges via gh pr merge after CI. Main session fast-forwards. | Shared repo, code review culture, want a PR paper trail and a CI gate. |
worktree-local-merge | Agent works in worktree/<slug>, main session fast-forwards via git merge --ff-only (no PR). | No GitHub remote, or solo work where PR ceremony is not worth it. |
direct-on-head | Agent commits directly on the head branch. Scope-restricted. | Personal projects where speed matters more than review: a blog, a notes vault, your own dotfiles repo. |
A repo can enable any subset. On this blog I enabled all three: direct-on-head for routine content edits, worktree-pr for any post that an agent drafts in isolation (these series posts, for one), worktree-local-merge for the rare moment I want isolation without the GitHub round-trip. Day-to-day is heavier on direct-on-head than I expected: about 80 percent of evenings I am editing prose in src/content/blog/, that path is in the authorized scope, the agent commits and pushes without asking. The other 20 percent goes through worktree-pr because I want the PR diff to scroll through before merging.
A first run, end to end
You cd into the repo and type /nf-git-workflow all (or /nf-git-workflow for the interactive picker). The skill:
- Runs preflight. Confirms you are inside a git repo. Confirms cwd is the main checkout, not a worktree. Detects the current branch (
git branch --show-current). Refuses if HEAD is detached. - Asks where to write the rule. Default is
.claude/rules/git-workflow.md. IfCLAUDE.mdexists at the repo root, also offers to append a marker-wrapped section there. - If the target file exists, checks for the install marker
<!-- nf-git-workflow:install -->. Marker present: ask “replace?” Marker absent: ask “overwrite?” - Assembles the rule from a header, one section per enabled mode (canonical order), and a footer. Substitutes the head branch name and install date.
- Writes the file. Prints a summary: path written, modes enabled, head branch protected, TODO items the user must fill in.
- Reminds you to reload the Claude Code session. Rules are read into the system prompt at session start.
After the install, you open the file and fill in the TODO scaffolds (preconditions, authorized scope, NOT-authorized scope). The skill seeds with generic examples but does not guess.
The rest of this post walks through the four design decisions inside the skill and the incidents that drove each one.
Decision 1: The head branch is detected at install, never re-evaluated
The skill records head_branch = git branch --show-current at install time and substitutes it into the rule file as a literal string. The rule then talks about that branch by name. No runtime detection, no “whatever branch you happen to be on now.”
An earlier draft did the elegant thing instead: store a placeholder and resolve it dynamically. If the project ever renamed main, the rule would just keep working.
Then I imagined the failure mode. Someone runs the skill while sitting on a hotfix branch. The skill records head_branch = hotfix-foo. Every subsequent agent commit lands on hotfix-foo without confirmation. Two days later the hotfix is merged and deleted, the rule still names hotfix-foo as protected, and the agent is now on main with no protection at all.
Dynamic resolution is worse. “Whichever branch you are on right now” means the rule is correct only when the user is on the branch they meant to protect, and silent otherwise. The whole point of the override is to declare a specific branch is the safe one. Pinning the name at install time is the correct semantic.
That drives two preflight checks the skill refuses to skip:
- Refuse on detached HEAD.
git branch --show-currentreturns empty. The skill cannot record nothing as the protected branch, and “main” is a guess. - Refuse from inside a worktree. Detect via
git rev-parse --show-toplevelversusgit rev-parse --git-common-dir. Installing from a worktree would record the worktree branch as protected, which is temporary and will be deleted next week.
The skill never tries to infer the “real” head from a worktree. That kind of guess goes silently wrong and surfaces later as “why is the agent committing to a deleted branch.”
Decision 2: Generator pattern, not runtime rule
The skill could have been a runtime hook: a script that intercepts every git commit and git push, checks the current state against some in-memory config, and authorizes or asks. That is the smart version.
I picked the dumb version. The skill emits a static markdown file. Claude Code reads it at session start as part of the rules pipeline. No daemon, no hook, no runtime evaluation, no extra failure mode.
Three reasons. Claude Code already reads rules: a markdown file in .claude/rules/ is the platform’s native path, and the override should ride that path instead of inventing a parallel one. The rule file is reviewable: open it, grep it, git blame it. A hook is opaque; a markdown rule is documentation. Debugging is grep: when the agent does something I did not expect, “does the rule allow this?” is one open in $EDITOR, not “let me read the hook source and reason about which branch fired.”
The trade-off is small. Re-running the skill requires a session reload because CC loads rules at session start. I list that in the install summary and it has not been a real friction in practice. Install once, reload once, forget the skill exists.
Decision 3: Multi-mode picker, not one-size-fits-all
The first draft had one mode: worktree-pr. Safest, full PR audit trail, CI gate. What is there to argue about?
The argument arrived the third week of using the skill. I was editing a typo in a published post. Spent two minutes typing the fix, then five minutes waiting for the worktree to spawn, the agent to commit, the PR to open, CI to run, the PR to merge, and the main session to fast-forward. For a one-character change. The ceremony was longer than the work.
I added direct-on-head next. Same safety guarantees on the head branch (no force-push, no hard-reset, no destructive ops), but the agent commits directly. No worktree, no PR, no CI gate. The mode is scope-restricted: only paths the user lists explicitly are auto-authorized; anything outside still requires confirmation.
That fixed the typo case but exposed a third one. Personal repo with no GitHub remote: worktree-pr is impossible, direct-on-head feels lazy when isolation is cheap. That is worktree-local-merge. Same isolation as worktree-pr, but the main session pulls the worktree branch in via git merge --ff-only instead of opening a PR.
So three modes, and a project can enable any subset. They are not exclusive because the right mode depends on the task, not on the repo. This blog uses all three. A pure-content notes vault would only enable direct-on-head. A shared client repo would only enable worktree-pr. The picker lets the project owner decide which combinations are possible inside that repo, and the agent picks the best fit per task.
A fourth mode I considered and rejected: “raw direct, no scope restriction.” The argument for it was “for repos I trust completely, why bother declaring scope.” The argument against was the typo case in reverse: an unrestricted agent on a head branch will eventually touch package.json or astro.config.mjs or wrangler.toml without asking. That is the kind of edit that goes wrong loudly. Scope restriction is cheap. Keep it.
Decision 4: TODO scaffolds for preconditions and scope, not auto-detection
The skill could try to be clever about preconditions. Read package.json, detect npm test and npm run lint. Read pyproject.toml, pull pytest. Read Cargo.toml, emit cargo test. The auto-detection is straightforward.
I tried it. The first version had a small detector that scanned package.json and seeded the preconditions list. Within a week I hit two failure modes, both on this blog.
First, the repo has package.json and the detector found npm test. There is no test script defined; npm test exits non-zero with “no test specified.” If the agent took the precondition seriously, every push would gate on a failing command. If the agent skipped failing preconditions, the mechanism was undermined. Either way the detector was actively wrong.
Second, the actual content-quality check on this repo is the em-dash grep against src/content/blog/<file>.md, documented in .claude/rules/no-ai-dash-tells.md. The detector did not know about it. There is no way it could. No generic detector would surface a project-specific grep as a precondition for deploy.
Both failures came from the same root: the detector pretended to know things it did not. A wrong default is more harmful than no default, because the user trusts the default and stops reading. After two debug sessions of “the agent ran a precondition that does not exist, why,” I ripped the detector out.
What replaced it is a TODO scaffold. The footer of the generated rule file says:
## Preconditions before auto-merge / auto-deploy / push to `main`
TODO: project owner fills in. Examples:
- Tests pass (`npm test`, `pytest`, `cargo test`, ...)
- Lint clean (`npm run lint`, `ruff check`, ...)
- Custom content checks (e.g. for blog: em-dash grep per project rule if present)
- Type check clean (`tsc --noEmit`, `mypy`, ...)
The examples line is hand-written prose, not a guess at what the repo actually uses. The TODO: marker is visible. The user fills it in once and the agent reads the filled-in list on every subsequent session. Same shape applies to the direct-on-head authorized scope and the shared NOT-authorized scope. The skill seeds with generic examples and leaves the actual list for the project owner.
The lesson generalizes: scaffolds are not the same as defaults. A scaffold says “fill this in.” A default says “this is correct unless you change it.” For project-specific configuration, the default is usually wrong somewhere, and scaffolds make the project owner notice.
The install marker pattern
One small detail that earned its keep early. The whole generated block is wrapped in HTML comment markers:
<!-- nf-git-workflow:install -->
... generated content ...
<!-- /nf-git-workflow:install -->
When the skill runs again it does not look at the file as a whole. It finds the open and close markers and replaces only the content between them. Anything before the open marker or after the close marker survives.
The markers exist because .claude/rules/git-workflow.md ends up co-located with hand-edited content: maybe a note at the top about why this override exists for this particular repo, maybe a sentence at the bottom for the next collaborator. The skill must not clobber that on re-run, and the markers make the replacement deterministic. The pattern is borrowed from direnv, from gitignore block-marker tools, from any installer that has to be idempotent on top of a user-editable file. Cheap to add, pays off the first time you flip a mode on or off.
What you should keep even if you never use my skill
Four patterns generalize cleanly:
- Per-repo overrides of global safety rules are legitimate. The global rule (“ask before every commit”) is correct for the population. Specific repos are outliers where the trade-off flips. Encode that, do not delete the global rule and hope you remember the exceptions.
- Detect once at install, store forever in a file. Anything you want stable across sessions should be a literal string in a generated file, not a runtime detection. Dynamic resolution sounds smart and is wrong any time the runtime context drifts from the install context.
- Three-mode taxonomy (isolated + reviewed, isolated + local, direct + scoped) generalizes. The same shape applies to deploy strategies (staging + canary + direct prod), schema migrations (PR + rehearsed apply + ad-hoc edit), feature flags (gated + percentage + raw). When you want “the safe one” plus “the fast one” plus “something in between,” name the three explicitly.
- Install markers (sentinel comments) make installers idempotent. Any tool that writes into a file the user might also edit needs markers. Five lines of code, and the first hand-edit you preserve pays for the next ten installs.
The skill itself is just one way to encode those. The patterns are the value.
Get the skill
White-labeled, ready to drop into ~/.claude/skills/. Published in the claude-skills-toolkit repo alongside nf-agents and nf-dream.
git clone https://github.com/llawliet11/claude-skills-toolkit.git
cp -r claude-skills-toolkit/nf-git-workflow ~/.claude/skills/
Folder reference: claude-skills-toolkit/nf-git-workflow.
Verify in a new Claude Code session: type /nf-git-workflow and the skill should appear in the slash-command list. Then cd into the repo you want to authorize, run /nf-git-workflow (interactive picker) or /nf-git-workflow all, and reload the session. After install, open the generated .claude/rules/git-workflow.md and fill in the TODO scaffolds. Re-running the skill is safe; the install markers ensure a clean replace.
If you adopt it and find a mode or a preflight check I have not yet hit, open an issue on the repo.