Pre-computed context
Exec-bit backstop: !bash "${CLAUDE_PLUGIN_ROOT}/skills/commit/scripts/exec-bit-check.sh" --probe 2>/dev/null || echo "unavailable — run the check manually"
Config layer (user-global): !test -f "$HOME/.claude/source-control.md" && echo present || echo absent
Repository context. Gather first
Collect these with individual Bash calls, one command per call, never combined into a single invocation:
- Current branch,
git branch --show-current - Staged,
git diff --cached --stat | tail -1 - Unstaged,
git status --short | head -20 - Recent commits,
git log --oneline -5
The two pipes are the bound and belong in the command. The Bash tool returns a command's
complete output into context before there is anything to decide about, so a read-time cap ("read
only the summary line") bounds nothing; head and tail truncate before the result exists. These
are ordinary body Bash calls, not pre-compute, so the one-invocation composition that applies to the
## Pre-computed context block does not apply here.
Then resolve the two repo-scoped config layers as separate calls. Each uses git's repo-root-relative
magic pathspec :/, which resolves against the top of the working tree, so these are correct from
any subdirectory and never need the repository root substituted into them. Quote the pathspec,
the leading : is pathspec magic, and quoting keeps the shell from reinterpreting it.
- Team layer, trackedness.
git ls-files --error-unmatch -- ":/.claude/source-control.md" - Team layer, existence.
git ls-files --cached --others -- ":/.claude/source-control.md" - Personal overlay, existence.
git ls-files --cached --others -- ":/.claude/source-control.local.md"
Probes 2 and 3 deliberately omit --exclude-standard, so a gitignored file is still listed: the
personal overlay is expected to be gitignored, and an untracked team file has to be seen before it
can be reported as untracked.
Combine probes 1 and 2 in that order. Trackedness first, existence consulted only when trackedness comes back nonzero. They are not independent readings:
| Trackedness (1) | Existence (2) | Team-layer state |
|---|---|---|
| exit 0 | not consulted | present (tracked), this is a config layer |
| nonzero | non-empty output | present but UNTRACKED, deliberately not a config layer |
| nonzero | empty output | absent |
A single --error-unmatch call cannot separate the last two rows; it exits nonzero for both. That
is the whole reason probe 2 exists.
A nonzero exit from probe 1 is a result, not a failure. It answers the question, and probe 2
resolves which answer it is. The unknown-value rule is narrower than that: only a probe that could
not run at all (git unavailable, not a repository) yields an unknown value to carry on past. Never
fold present but UNTRACKED into "unknown". They mean opposite things, and treating the untracked
case as unknown is exactly what readmits a file the resolution rules exclude.
Keep the git lines above out of ## Pre-computed context: the harness composes that block into one
shell invocation, and a worktree-isolated agent refuses a git-bearing compound command it cannot
statically verify.
These are snapshots taken when they run, not substitutes for the checks. The exec-bit
line only sees what was already staged when the skill loaded; anything staged in step 2 below is
invisible to it, so step 4 re-runs the script for real. The config-layer probes report presence,
not merged content. Read the layers that are present and merge them per key. A tracked-team layer
reported present but UNTRACKED is deliberately not a layer: resolution requires the team file to
be git-tracked, so an untracked or gitignored file at that well-known path must not drive the
convention.
Every repo-scoped probe anchors at the repository root, via the :/ magic pathspec, never at
the session's current directory. A session started in a subdirectory would otherwise look for
<cwd>/.claude/ and report both repo-scoped layers absent. Silently dropping the team convention
and trailer_policy, and producing a commit with the wrong subject shape or attribution. Never
substitute a resolved root path into a probe instead: a repository root containing $(…), a
backtick, or a double quote is not made safe by wrapping the substituted text in double quotes
(the substitution still evaluates, or the quote terminates the argument), and :/ never
substitutes the root at all. This matches the root-resolution requirement
${CLAUDE_PLUGIN_ROOT}/reference/config-resolution.md
already states for resolution itself; the probes must not disagree with it. exec-bit-check.sh
anchors itself the same way.
Per-commit checklist
Run this list for every commit, including the second and tenth commit of a session. These are commands, not remembered facts. Steps 3–5 are the ones a long session silently stops performing.
- Resolve the convention +
trailer_policyacross all three config layers (once per session is enough; re-resolve if a layer changed). - Stage surgically by explicit path, after checking the four pre-existing conditions below.
- Format, run the discoverable formatter scoped to this commit's paths, re-stage its fixes.
- Exec-bit, run
exec-bit-check.sh --fix -- <this commit's paths>, AFTER step 3. - Pre-check the drafted subject against the resolved pattern before invoking git.
- Commit via the Bash tool:
git commit -F -heredoc-piped,--trailerpertrailer_policy. - Report the resulting SHA + subject to the user.
Purpose
Encapsulates the canonical mechanic for building a commit message that honors a subject convention,
appending a Co-authored-by: trailer, and feeding the result to git commit via stdin, without
these failure modes:
- PowerShell here-string syntax (
@'...'@) inside a Bash tool call producesunexpected EOFand triggers fallback to writing the message to.git/<TEMP>.txt..git/is git's internal directory; scratch files there collide withCOMMIT_EDITMSGand other internals. git commit -m "<multi-line>"flattens newlines unpredictably across shells.git add -A/git add .stages secrets, build artifacts, unrelated edits, the convention is surgical staging.
Subject convention ladder
The subject convention (WHAT shape a subject must take) resolves via a ladder, checked in order:
- The layered
source-control.mdconvention config. User-global, tracked team, and gitignored personal overlay, merged per key by${CLAUDE_PLUGIN_ROOT}/reference/config-resolution.md. Resolve all three layers before using any value; a value from one layer alone is not the effective config. When any layer declares one, the effectivesubject_pattern(andtype_list, when Conventional-Commits-shaped) is authoritative. Before pre-checking, expandsubject_pattern: the literal keywordConventional Commitsexpands to the anchored pattern in step 3 below; anything else is already a single anchored regex and is used as-is. - The consuming project's own
CLAUDE.md,AGENTS.md, rules, or commit-msg hook, if the effective merged config declares nosubject_pattern(absent from every layer, which includes the case where layers exist but only contribute other keys) but the project declares (or enforces via lefthook, husky, commitlint, or a plain.git/hooks/) a different convention, follow that instead. - Conventional Commits (11-type vocabulary), the default when neither of the above is present. Every subject must match this anchored pattern:
^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+
Compliant examples:
feat(auth): add OAuth login flowfix(api): handle null user in /me endpointdocs: clarify rebase guidancerefactor(skills)!: rename /simplify to /code-review
The 11 types. build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test. Come from
the Conventional Commits spec, the Angular convention, commitlint's @commitlint/config-conventional
source, and amannn/action-semantic-pull-request's default types list; all four agree on this exact
set. security is not a Conventional Commits type in any of them, never add it.
When the project enforces its convention with a commit-msg git hook, that hook is the authoritative
gate regardless of which rung above resolved the pattern; this skill's pre-check exists to fast-fail
client-side before the hook round-trip. When no layer resolves a subject_pattern and nothing is
inferable from the project's own files, point the user at /source-control:setup to persist a
convention instead of re-inferring one every commit.
Task
Survey working tree (
git status,git diff --cached --stat) to confirm what's staged.Stage the intended files, but first check each path against these four pre-existing conditions, before this step touches anything. Each is a state
git addsilently overwrites rather than errors on, and in three of the fourgit diff --statreads empty, so the obvious check misses it. Rationale for each is in reference/staging-preconditions.md.Condition Detect with Action Already-staged deletion git diff --cached --name-status -- <path>reportsDSkip the path entirely; never git addover itStaged rename, old side same command reports R<score> <old> <new>whose<old>falls under<path>Skip the old side entirely; never git addover itUntracked files under a directory path git status --porcelain -- <path>shows??entriesStop and surface them; enumerate the intended files instead of adding the directory Partial-staging split git diff --cached --stat -- <path>andgit diff --stat -- <path>both non-emptyLeave as-is; a blanket add sweeps in hunks the user deliberately left unstaged Run
git add <path>...only for paths in none of those four states.Run the format-before-push check against the files just staged for this commit (not the full staged set), re-staging any fixes.
Run the exec-bit check, after step 3, not before: a formatter re-stage in step 3 reads the worktree file mode, so setting the exec bit any earlier would be silently undone by that later
git add.bash "${CLAUDE_PLUGIN_ROOT}/skills/commit/scripts/exec-bit-check.sh" --fix -- <path>...The script reports and fixes every newly-added file whose staged blob starts with a shebang while its staged mode is
100644, setting the worktree bit and the index entry in the order that survives a latergit add. The pathspec is required.--fixrefuses an unscoped run (exit 2) rather than sweeping mode changes across another session's staged work; pass--allonly to opt into a deliberate whole-index sweep. Rationale, cross-platform caveats, and the manual fallback live in reference/exec-bit.md.Draft a subject + optional body, scoped to the staged diff, shaped to satisfy the active subject convention (default: the Conventional Commits pattern above).
Pre-check the subject against the pattern (fast-fail before invoking git).
Invoke
git commit -F -via the Bash tool, heredoc-piped, adding--trailerper the trailer template below only when the resolvedtrailer_policycalls for one.Surface the resulting commit SHA + subject to the user.
Canonical bash form
Use the Bash tool, not the PowerShell tool. Bash heredoc is the canonical form across all platforms (Git Bash, Linux, macOS).
# --trailer line below applies only when the resolved trailer_policy calls for one
# (default: yes). trailer_policy "none" -> drop the --trailer line entirely.
# trailer_policy naming a different template -> substitute that template's text.
git commit -F - --cleanup=verbatim \
--trailer "<Co-authored-by trailer, placeholders filled>" \
<<'EOF'
<subject — shaped to satisfy the active subject convention>
<body — wrap at ~72 cols, blank line between paragraphs>
EOF
PowerShell here-string is shown only for reference. git commit -F - <message-via-stdin> with
@'...'@ would work in a pure PowerShell tool call, but never mix syntaxes inside one tool
invocation. A PowerShell @'...'@ block inside a Bash tool call leaves the bash parser unable to
terminate the heredoc.
Hard rules:
- Invoke via the Bash tool.
- Heredoc delimiter is single-quoted:
<<'EOF' ... EOF. Single quotes prevent$variableand backtick interpolation inside the message. --cleanup=verbatimpreserves the message exactly, no auto-stripping of comments or whitespace.- Never write the message to
.git/<TEMP>.txt. If a real file is unavoidable, usemktemp. - Never mix Bash heredoc with PowerShell
@'...'@inside one invocation.
For a dirty shared index (concurrent sessions, pre-existing mixed WIP), the pathspec-limited form and its staged-deletion hazards live in reference/pathspec-commits.md. Default remains the plain index commit.
The exec bit does not survive the pathspec form under core.filemode=false (the Windows
default). --only records the working-tree mode, and with filemode off git cannot see the
chmod +x, so a correctly-set 100755 index entry is rebuilt as 100644. Verified both
directions: the plain index commit preserves 100755; the pathspec commit loses it. If this
commit includes a newly-added shebang file the exec-bit check corrected, commit that path with the
plain index form (splitting the commit if the rest needs a pathspec), and confirm with
git ls-tree HEAD -- <path> rather than trusting the index. Full detail and the two workarounds
that were tested and failed are in reference/pathspec-commits.md.
Pre-check
Before invoking git commit, regex-match the drafted subject against the active convention's pattern
(default: the Conventional Commits pattern above).
On mismatch, surface the convention name and the compliant examples as actionable guidance, and ask
for a compliant subject before invoking git. The pre-check is shape-only; the project's commit-msg
git hook (when one exists) is the authoritative gate at commit time. The pre-check exists to save the
hook-startup floor when the drafted subject is obviously wrong, plus to give the user a fast
actionable error rather than an opaque hook failure.
Trailer resolution
The default template is:
Co-authored-by: Claude <model> <noreply@anthropic.com>
The <model> placeholder is filled from your own knowledge of the running session: the model
family and version the harness reports for this session (the value /usage shows). If uncertain,
invoke /usage to confirm before committing. There is no
environment variable that auto-fills it, the trailer is part of the message body sent to
git commit, not git config.
An optional context clause, Co-authored-by: Claude <model> (<context>) <noreply@anthropic.com>,
e.g. (1M context), may be added when the context window is a genuinely distinguishing fact about
the session and is known with confidence. It is not required, and its absence is not a defect.
Which authority wins
Four sources can specify a trailer. Resolve in this order:
- A resolved
trailer_policyfrom any config layer. Authoritative.nonemeans omit--trailerfrom thegit commitinvocation entirely (do not append an empty or default trailer); a value naming a different template means substitute that template exactly.noneand absence are different states. - The consuming project's own conventions (
CLAUDE.md,AGENTS.md, rules), if they specify a different attribution trailer, or none, follow those. - Harness-injected commit guidance. The Claude Code harness may inject its own commit
instruction into the session, naming a
Co-Authored-Bytrailer. Adopt its shape, this is why the context clause above is optional rather than mandatory, but never copy its literal text. That injected guidance can carry a hardcoded model name that does not match the running session. Copying it verbatim writes a false provenance claim into durable git history, which is precisely the harm this template exists to avoid. Always fill<model>from actual session knowledge. - This skill's default, above.
Key spelling. This skill emits Co-authored-by, the spelling GitHub's own documentation uses
exclusively and the one GitHub itself writes when it appends co-author trailers to a squash-merge
message. GitHub's attribution is not case-sensitive in practice (a commit carrying a
Co-Authored-By: trailer resolves its co-author in the GraphQL Commit.authors connection just the
same; the docs do not state case sensitivity either way), so this is a consistency choice, not a
correctness one: branch commits and the forge-written squash merges agree. Git preserves a trailer
key's case verbatim (git interpret-trailers does not normalize it, and no trailer.* config here
changes that), so existing history keeps whatever spelling it was written with; this skill never
rewrites it. A consumer who wants a different spelling expresses it as a trailer_policy template.
Unrelated uncommitted changes
If the working tree contains unstaged or untracked files that fall outside this commit's scope,
classify each before staging per /source-control:pull-request create (its unrelated-changes classification:
include / stash / separate-commit / discard). Do not duplicate that classification here; invoke
/source-control:pull-request create to surface it to the user.
Staging discipline
Always git add <specific-files>, never git add -A or git add .. The risk is including secrets,
build artifacts, or unrelated changes that the user did not approve for this commit. If multiple
files are intentionally part of the commit, stage them by explicit list, not by wildcard, and skip
the partial-staging split per step 2 above.
Immediately after staging, run the format-before-push check and then the exec-bit check, in that order. Catch what CI would otherwise catch on the push round-trip.
Composition policy
This skill is the single source of truth for the commit mechanic. Other skills should compose
/source-control:commit rather than invoking git commit directly. Direct calls bypass the pre-check, trailer
logic, and surgical-staging discipline this skill exists to enforce.
"Compose" means one of exactly two things, and a composing skill must name which:
- Re-invoke
/source-control:commitfor the commit. This is the default and the only form that re-renders this skill's full content. Prefer it whenever the commit is a discrete step a user could have asked for. - Run the per-commit checklist above yourself, per commit, as commands. Permitted for a workflow skill making many commits in one flow, where re-invocation per commit is disproportionate.
What is not composition is treating this skill as a convention absorbed once and thereafter remembered. The mechanic that decays is not the shape of the commit message, that is reinforced every commit, it is the ordered per-commit checks: the formatter run, the exec-bit fix, the subject pre-check. Those produce no visible signal when skipped, so a session that has stopped running them looks identical to one that has not, until CI says otherwise. That is the observed failure this policy exists to prevent, not a hypothetical one.
Two properties make step 4 the cheap re-anchor for the checklist form: it is a command with an exit code rather than a paragraph to recall, and the pre-computed probe at the top of this file surfaces its finding before anything else is read.
Workflow skills without explicit commit semantics should report status at phase boundaries and let
the user or the next workflow stage decide commit timing; a skill with commit semantics in its
documented contract (e.g. /source-control:pull-request create) composes this one.
Recorded deviations
- Model invocation stays enabled (
disable-model-invocation: false, declared explicitly rather than left to the default). The fleet archetype for a/source-control:commit-shaped skill isdisable-model-invocation: true. User-invoked only. This skill deliberately deviates because its composition design requires the model to reach it: a workflow skill that composes the commit mechanic cannot do so if the model may not load it. Compensating controls: surgical staging (nevergit add -A), the subject pre-check before any git invocation, no hook bypass, and a user-visible SHA + subject report after every commit.
What this skill does NOT do
- No
git push, that's/source-control:pull-request create. - No branch creation, that's the project's branch-naming / branch-protection mechanisms (or
/source-control:worktree create). - No PR body composition, that's
/source-control:pull-request create. - No
git merge/gh pr merge, that's/source-control:pull-request merge. - No rebase, that's
/source-control:pull-request create. - No
--no-verifyor hook bypass, if the project'scommit-msghook rejects the message, surface the error and re-draft; never bypass.
Next
/source-control:pull-request create. Its precondition is a branch carrying the work to ship.
Reference index. Load on demand
| File | Load when |
|---|---|
| reference/staging-preconditions.md | A step-2 precondition fired and the right action is not obvious, or judging whether a new condition belongs in the set. |
| reference/format-check.md | Running step 3, formatter discovery, path scoping, the two-stage filter, partial-staging preservation. |
| reference/exec-bit.md | Understanding or hand-running step 4, why both the worktree bit and the index, core.filemode caveats, manual fallback. |
| reference/pathspec-commits.md | The index holds staged work outside this commit's scope. Pathspec --only semantics and the staged-deletion hide/restore sequence. |
${CLAUDE_PLUGIN_ROOT}/reference/config-resolution.md |
Resolving or merging the three config layers, or interpreting a key's none-vs-absent state. |