git-publish — offer to commit and push, with confirmation
Why this is a skill (Form 1) and not Form 2
Two other pieces need to chain into this one by name: project-initializer (after
/init-project finishes) and /new-feature (after java-spring-boot-developer reports
success). disable-model-invocation: true (Form 2) blocks exactly that — the model
can't call a Form 2 skill via the Skill tool at all, only a human typing /git-publish
can, the same restriction java-spring-boot-developer.md documents for why it can't
re-invoke java-patterns. Precedent: @.claude/decisions/0007-pipeline-skills-invocation.md
(D17) — the five /new-feature pipeline skills stay without disable-model-invocation
for the same reason, and rely on an entry guard in the body instead of the flag. This
file's guard is the two AskUserQuestion gates below: no git side effect ever runs
without an explicit yes, regardless of what triggered the invocation.
Form 3 (agent) was rejected: the confirmation dialogue with the user is the heart of
this task, the context it needs fits entirely in this file, and the final output is
short — all three fail the counter-test in references/decision-matrix.md § 5.
Decision record: @.claude/decisions/0034-git-publish-skill.md.
Contract
Input (optional, from the invoking context): a short description of what was just
done — e.g. "initial scaffold, blueprint hexagonal, maven, features: rest,jpa" or
"UC-001-order: order management feature". Used to build the commit message. No input →
generic message from what git status shows.
Input (optional): paths to stage. When the invoking context names paths — /new-feature
does, for an approved spec not implemented yet (docs/use-cases/UC-NNN-<slug>/,
docs/use-cases/BACKLOG.md) — gate 1 lists and stages only those paths. Anything else
dirty in the tree stays unstaged and is named in the report.
Reads: git status, git remote -v, gh auth status. Nothing in the project files.
Writes: .git/ of the current project, and — only after the second confirmation —
a new GitHub repository via gh repo create, or a push to a remote the user names.
Never writes project source files.
Integration:
- Invoked by
project-initializer(agent) right after a green build, via theSkilltool, with a scaffold summary as context. - Invoked by
/new-featureat its end: afterjava-spring-boot-developerreports success, with the UC name/summary as context; or, when the user declines implementing now, with the approved spec's paths as the only paths to stage. - Invocable directly by the user (
/git-publish, or by asking in plain language).
Procedure
1 · Read state, never assume it
git rev-parse --is-inside-work-tree 2>/dev/null && echo TRACKED || echo UNTRACKED
git status --porcelain 2>/dev/null
git remote -v 2>/dev/null
git log -1 --oneline 2>/dev/null
git check-ignore -q . && echo ROOT_IGNORED_BY_PARENT
ROOT_IGNORED_BY_PARENT — this project's own root is gitignored by an outer repo
(e.g. a demo under this meta-repo's own examples/). git status --porcelain reflects
the outer repo's tracked files, which have no relation to the feature this run just
implemented — src/, docs/use-cases/, all of it invisible to that status.
Before gate 1's question, show the diff-file list and ask explicitly whether it actually
matches the feature just built; don't let the caller's commit-message context imply it
does. If the caller (/new-feature's guardrail step 2) already flagged this, skip
re-discovering it — just carry the warning into gate 1's question.
Three states, three different phrasings for gate 1:
| State | Gate 1 asks |
|---|---|
UNTRACKED (no .git) |
"Initialize git and create the first commit?" |
TRACKED, dirty (git status --porcelain non-empty) |
"Commit these changes now?" |
TRACKED, clean |
Skip gate 1 — nothing to commit. Go straight to gate 2 only if there are unpushed commits or no remote configured; otherwise report "nothing to do" and stop |
2 · Gate 1 — local commit
AskUserQuestion, options: Yes, commit now / No, skip. "No" stops here — report
what would have been committed and don't touch anything.
On "Yes":
git initonly ifUNTRACKED.Show
git statusand scan the file list for anything that looks like a secret before staging —.env,*.pem,*.key,credentials*,*secret*. If any match, stop and ask the user to confirm or exclude them; never stage a likely secret silently.git add -A(respecting.gitignore, already generated by the Initializr) — or, when the input named paths,git add -- <paths>with only the paths that exist, and nothing else.git commit -m "<message>"— build the message from the input context following Conventional Commits:chore: initial project scaffold — <blueprint/build/features>for a project-initializer call,feat(UC-NNN-slug): <summary>for a/new-featurecall after the executor,docs(UC-NNN-slug): approved specfor a docs-only one, or a genericchore: commit pending changeswith no context. Always append:Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3 · Gate 2 — remote create/push
Only reached after gate 1 commits successfully, or when TRACKED+clean with unpushed
commits/no remote. AskUserQuestion, options:
- Create a new GitHub repo (gh) and push — needs
gh. Rungh auth statusfirst; not authenticated → report the exact command to fix it (gh auth login) and stop, never attempt to log in on the user's behalf. Ask visibility (public/private) as part of the same question. Then:gh repo create <artifactId> --<public|private> --source=. --remote=origin --push - Push to an existing remote — ask for the URL if
git remote -vshowed none;git remote add origin <url>(orset-urliforiginexists and the user confirms overwriting it), thengit push -u origin <current-branch>. - Skip, keep local only — stop, report the local commit is done and nothing was pushed.
Never git push --force, never --no-verify, never delete an existing remote without
the user naming that as the explicit choice.
4 · Report
✅ Committed <short-hash> — "<message>"
<✅ Pushed to <remote-url> (branch <name>) | ⏭️ Local only, not pushed>
Failure modes
gh missing or not authenticated:
❌ gh not authenticated. Run `gh auth login`, then re-invoke this skill to push.
Local commit is already done — nothing lost.
Suspicious file staged:
⚠️ .env matched the secret-file scan. Excluded from `git add`.
Add it to .gitignore, or confirm explicitly if it must be committed.