Offers to git init/commit the current project and, optionally, create a GitHub repo (via `gh`) and push — always behind two explicit confirmations, one for the local commit and one for the remote create+push. Use right after a project was just generated by `/init-project`, right after `java-spring-boot-developer` reports a feature implemented successfully, when `/new-feature` ends with an approved spec, or whenever the user asks to create a git repo, commit, or push the current project.
Installs into .claude/skills of the current project.
Are you the author of Git Publish?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/nerviz-ai-git-publish)
---
name: git-publish
description: >
Offers to git init/commit the current project and, optionally, create a GitHub repo
(via `gh`) and push — always behind two explicit confirmations, one for the local
commit and one for the remote create+push. Use right after a project was just
generated by `/init-project`, right after `java-spring-boot-developer` reports a
feature implemented successfully, when `/new-feature` ends with an approved spec, or
whenever the user asks to create a git repo, commit, or push the current project.
allowed-tools: AskUserQuestion, Bash(git init:*), Bash(git add:*), Bash(git commit:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git remote:*), Bash(git rev-parse:*), Bash(git check-ignore:*), Bash(gh auth status:*), Bash(echo:*)
model: sonnet
effort: low
---
# `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 `gof-design-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`.
Runs on `sonnet` with `effort: low`: a fixed procedure whose every side effect waits
behind the two gates (`@.claude/decisions/0080-procedural-skills-on-sonnet.md`).
## Contract
**Class:** ops — the territory is `skill_classes.ops` in
`@.claude/schemas/extensions.json`, and it is **empty**: this skill writes no file. Its whole
effect is `git` over `Bash`, which the guard deliberately does not cover — a commit is not a
file write, and the two confirmations below are what gate it. `allowed-tools` pre-approves
only the local `git` subcommands and `gh auth status`; the remote step is left to
`permissions.ask` (decision 0076).
**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.
**Input (optional): project directory.** When the invoking context names
`project: <absolute path>` — `project-initializer` does, because it runs in a session opened
somewhere else — that directory is the project. Run every command of this procedure as
`(cd "<path>" && git …)`, `gh repo create` included, so `--source=.` is the project and
`git init` never lands in the session's own repository. Without it, the project is the
session's directory.
**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 the `Skill`
tool, with a scaffold summary as context.
- Invoked by `/new-feature` at its end: after `java-spring-boot-developer` reports 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
```bash
git rev-parse --is-inside-work-tree 2>/dev/null && echo TRACKED || echo UNTRACKED
git status --porcelain 2>/dev/null
git diff --cached --stat 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.
Four states, four 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 |
| `TRACKED`, and `git status --porcelain` shows paths **outside** the ones the caller named | "Commit only this run's paths, or everything that is dirty?" — two options, and the question comes **before** gate 1's own |
**`PRE_EXISTING_WORK`** — the fourth state. Changes that belong to no caller of this skill:
someone else's cleanup, a deletion made by hand, a half-finished edit, an untracked file from a
previous run. **Read from `git status --porcelain`, not from the index alone** — an unstaged
edit and an untracked file are invisible to `git diff --cached` and `git add -A` commits them
anyway (lessons-learned-014 § 11).
**`.claude/audit-usage/**` never counts toward this state**: step 2.3 stages it with every
run, and the previous run's report lands after that run's commit by construction
(lessons-learned-016 § 8). A worktree whose only extra paths are under it is the plain
dirty state, not this one.
Deciding it silently is what went wrong in lessons-learned-012 § 7 — 1.503 staged
deletions rode along with a feature commit, and only a manual read of the list caught it.
Show the paths, say which of them the caller named, and ask with those two options. On
"only this run's paths", stage with `git add -- <paths>` and never `git add -A`; the rest
stays dirty and is reported as left behind, not reverted. On "everything that is dirty",
**two commits, one gate, one push**: first the run's paths plus `.claude/audit-usage/` with
the run's message (step 2.4), then the rest with `git add -A` and
`chore: commit changes from before this run`, whose body lists every path it carries. One
commit for both gives the run's type to content the run did not write — a
`docs(UC-…): approved spec` once shipped a build-plugin change and a new compose service
(lessons-learned-016 § 9). The secret scan of step 2.2 covers both sets before either is
staged. `/new-feature`'s guardrail step
2 already flags this at the start of the run and **hands over the list itself** — when it did,
use that list instead of rediscovering it, and diff it against what is dirty now: what the
guardrail saw is what the run did not produce.
### 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":
1. `git init` only if `UNTRACKED`.
2. Show `git status` and 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.
3. `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.
**`.claude/audit-usage/` is one of those paths, always**, whether or not the caller named
it: the trail of the run belongs to the commit of the run that produced it. Only
`.state/` is ignored, and `.gitignore` already handles that. Staging it in one run and
leaving it out of the next is what makes every later diff start dirty
(lessons-learned-012 § 15). This skill leaves no report of its own — its class, `ops`,
declares `audited: false` in `skill_classes` of `.claude/schemas/extensions.json` — because
that report could only be written after this commit, and every publish would end dirty.
4. `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-feature`
call after the executor, `docs(UC-NNN-slug): approved spec` for a docs-only one, or a generic `chore: commit pending changes` with no context.
**Attribution: append exactly the trailer the session provides, and nothing else.** The
running session states its own attribution lines; use them verbatim. **When it states none,
the commit carries none** — no `Co-Authored-By` at all.
**Never write a model name from memory.** Same discipline `@CLAUDE.md` invariant 8 applies to
Java and Spring Boot versions, and for a sharper reason here: this file is copied into every
generated project, so a literal pinned in it goes stale in every one of them, silently and
forever. A run under one model committed with another model's name because this step spelled
one out, and the audit trail then recorded two models on one feature (lessons-learned-014
§ 10). A wrong attribution is worse than an absent one — it is in the git history for good.
### 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`. Run `gh auth status` first;
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:
```bash
gh repo create <artifactId> --<public|private> --source=. --remote=origin --push
```
- **Push to an existing remote** — ask for the URL if `git remote -v` showed none;
`git remote add origin <url>` (or `set-url` if `origin` exists and the user confirms
overwriting it), then `git 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.
The push itself raises a permission prompt after the gate's yes — `git push` and
`gh repo create` are in `permissions.ask` and deliberately absent from this skill's
`allowed-tools`, and `ask` is evaluated before any allow. That is the intended second look
at the one irreversible step, not a misconfiguration. A force push is refused outright by
`guard bash` (`guard.force_push` in `@.claude/schemas/extensions.json`), in every spelling
it can read.
### 4 · Report
```
✅ Committed <short-hash> — "<message>"
<✅ Committed <short-hash> — "chore: commit changes from before this run" (<n> paths) — only on "everything that is dirty">
<✅ 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.
```