Smart Commit
SkillCommunicationSmart batch commit. Analyzes uncommitted changes, groups by cohesion, generates commit messages matching project style. Default: output git commands for manual execution. With --execute: directly run git add + git commit (requires user approval). Use when: user says 'commit', 'batch commit', 'prepare commit', 'write commit message', or /smart-commit
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Smart Commit skill
What this skill tells your AI
The instructions your AI receives, as published by sd0xdev/sd0x-harness in skills/smart-commit/SKILL.md and read by ahel’s review.
Analyze uncommitted changes → group by cohesion → generate commit messages → output git commands (or execute directly with --execute).
Workflow
sequenceDiagram
participant C as Claude
participant U as User
C->>C: Step 1a: Detect permissions
C->>C: Step 1b: Learn commit style
C->>C: Step 1c: Identity diagnostics
C->>C: Step 1d: Signing diagnostics
C->>C: Step 1e: AI guard readiness
C->>C: Step 2: Pre-flight check (precommit)
C->>C: Step 3: Collect changes + exclude sensitive files
C->>C: Step 4: Group (high cohesion)
C->>U: Show commit plan (with Author/Signing/Guard)
U->>C: Confirm/adjust
loop Each commit group
C->>C: Read diff → generate message
C->>C: AI trailer sanitization
alt --execute mode
C->>C: Runtime validation → git commit
else manual mode
C->>U: Output git commands
end
end
C->>C: Step 6: Verification + post-commit leak detection
Git Environment Policy (applies to every step below)
Every git command this skill runs itself — read or write, either mode — resolves the same
question: which repository, tree, index, ancestry and configuration does it act on? The answer is
pinned two ways, both defined in git-environment.md § 1: the
inherited GIT_* variables are stripped, and every command carries -C "$REPO_ROOT".
Where the stripping happens depends on who runs git:
| Callee | How the policy is applied |
|---|---|
A fence running git inline | The canonical env -u prefix, written out literally — never through a variable, because zsh does not word-split an expansion used as a command prefix |
A helper that runs its own git commands and does not strip (git-profile.sh) | The prefix goes on the delegation, at the boundary |
A helper that strips the same list itself (smart-commit-inspect.sh, smart-commit-execute.sh) | No prefix — the script's own unset block is the policy, and prefixing it as well would state the same rule twice in two places that could drift |
Every /bin/bash -p -- "$INSPECT" … call is the third row — all ten of them, across Steps 1b–1e,
3, 4, 5a and 6 — because smart-commit-inspect.sh unsets the list once for the whole process.
$REPO_ROOT is still re-derived in every fenced block: each block is a separate shell, so nothing
carries over.
What the skill prints is governed by § 2 of that file — the same list, the same spelling.
Step 1: Detect Permissions + Learn Style
1a. Permission Detection
Read CLAUDE.md and .claude/rules/git-workflow.md to determine mode:
| Mode | Condition | Behavior |
|---|---|---|
| manual | No --execute flag (default) | Output commands only |
| execute | --execute flag passed | Execute directly (with user approval via AskUserQuestion) |
Default to manual mode. Direct execution requires explicit --execute flag regardless of project git restrictions.
--execute mode: When --execute is passed, use AskUserQuestion to show the full commit plan and get explicit user approval before executing. This is a skill-level exception to git-workflow rules (same pattern as /push-ci).
1b. Learn Commit Style
REPO_ROOT=$(/usr/bin/env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE -u GIT_OBJECT_DIRECTORY -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_NAMESPACE -u GIT_CEILING_DIRECTORIES -u GIT_GLOB_PATHSPECS -u GIT_ICASE_PATHSPECS -u GIT_NOGLOB_PATHSPECS -u GIT_LITERAL_PATHSPECS -u GIT_CONFIG -u GIT_CONFIG_PARAMETERS -u GIT_CONFIG_COUNT -u GIT_CONFIG_NOSYSTEM -u GIT_CONFIG_GLOBAL -u GIT_CONFIG_SYSTEM -u GIT_IMPLICIT_WORK_TREE -u GIT_GRAFT_FILE -u GIT_SHALLOW_FILE -u GIT_PREFIX -u GIT_NO_REPLACE_OBJECTS -u GIT_REPLACE_REF_BASE -u GIT_EXTERNAL_DIFF -u ALLOW_AI_COAUTHOR git rev-parse --show-toplevel && printf .) || { echo "⚠️ could not resolve the repository root — aborting" >&2; exit 1; }
REPO_ROOT=${REPO_ROOT%.}; REPO_ROOT=${REPO_ROOT%$'\n'}
INSPECT="$REPO_ROOT/.claude/scripts/smart-commit-inspect.sh"
[ -r "$INSPECT" ] || INSPECT="$REPO_ROOT/skills/smart-commit/scripts/smart-commit-inspect.sh"
[ -r "$INSPECT" ] || { echo "⚠️ smart-commit-inspect.sh not found — run /install-scripts" >&2; exit 1; }
/bin/bash -p -- "$INSPECT" style
Infer format, type vocabulary, subject conventions (capitalization/tense/ticket ID), and language from recent commits.
| Condition | Behavior |
|---|---|
| Commits listed | Infer the convention from them |
Non-zero exit, does not have any commits yet on stderr | Not an error — this is the first commit of a new repository, which is a case /smart-commit exists for. Fall back to the default <type>: <subject> convention and continue. Do not apply the "stop and report the stderr" rule the later steps carry: they read state a commit depends on, this one reads history that may legitimately be empty |
| Any other non-zero exit | Stop and report the stderr — the repository could not be read |
1c. Identity Diagnostics
Shared diagnostic (preferred path):
REPO_ROOT=$(/usr/bin/env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE -u GIT_OBJECT_DIRECTORY -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_NAMESPACE -u GIT_CEILING_DIRECTORIES -u GIT_GLOB_PATHSPECS -u GIT_ICASE_PATHSPECS -u GIT_NOGLOB_PATHSPECS -u GIT_LITERAL_PATHSPECS -u GIT_CONFIG -u GIT_CONFIG_PARAMETERS -u GIT_CONFIG_COUNT -u GIT_CONFIG_NOSYSTEM -u GIT_CONFIG_GLOBAL -u GIT_CONFIG_SYSTEM -u GIT_IMPLICIT_WORK_TREE -u GIT_GRAFT_FILE -u GIT_SHALLOW_FILE -u GIT_PREFIX -u GIT_NO_REPLACE_OBJECTS -u GIT_REPLACE_REF_BASE -u GIT_EXTERNAL_DIFF -u ALLOW_AI_COAUTHOR git rev-parse --show-toplevel && printf .) || { echo "⚠️ could not resolve the repository root — aborting" >&2; exit 1; }
REPO_ROOT=${REPO_ROOT%.}; REPO_ROOT=${REPO_ROOT%$'\n'}
/usr/bin/env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE -u GIT_OBJECT_DIRECTORY -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_NAMESPACE -u GIT_CEILING_DIRECTORIES -u GIT_GLOB_PATHSPECS -u GIT_ICASE_PATHSPECS -u GIT_NOGLOB_PATHSPECS -u GIT_LITERAL_PATHSPECS -u GIT_CONFIG -u GIT_CONFIG_PARAMETERS -u GIT_CONFIG_COUNT -u GIT_CONFIG_NOSYSTEM -u GIT_CONFIG_GLOBAL -u GIT_CONFIG_SYSTEM -u GIT_IMPLICIT_WORK_TREE -u GIT_GRAFT_FILE -u GIT_SHALLOW_FILE -u GIT_PREFIX -u GIT_NO_REPLACE_OBJECTS -u GIT_REPLACE_REF_BASE -u GIT_EXTERNAL_DIFF -u ALLOW_AI_COAUTHOR /bin/bash -p -- "$REPO_ROOT/scripts/run-skill.sh" git-profile git-profile.sh doctor --json
The prefix goes on the delegation, not inside the helper: git-profile.sh runs its own
git commands, and stripping at the boundary is what makes it and the fallback below agree. The
interpreter is /bin/bash -p -- for the reason given once in
execute-mode.md § Why the entrypoint is spelled /bin/bash -p -- —
it applies here too, and this is the path whose JSON decides HALT versus continue.
Known limitation. In a consuming project this delegation cannot resolve git-profile.sh,
so the fallback below is the normal path there — not a broken installation. Two stacked defects,
both shared by the eleven skills that delegate through the runner; the first could be fixed here
alone, but not without diverging this one skill from the other ten, and the second would remain:
4-implementation.md § 6.
If the script succeeds, parse the JSON output:
status: "ok"→ silent continue, useeffective_identityandsigningfieldsstatus: "warn"→ display warnings fromissues[], continuestatus: "halt"→ display halt issues, stop with guidance
If the script fails (not found, parse error, non-zero exit), fall back to the diagnostic below. Infrastructure failure = warn-only; never halt on fallback path itself.
Fallback — the claim is narrow: the two still share the root derivation, git and
/bin/bash, and nothing here survives those failing. What they no longer share is the
resolution — the preferred path goes through scripts/run-skill.sh, the fallback resolves
smart-commit-inspect.sh by path, installed copy first. A missing or broken runner therefore
takes out the first and leaves the second working. Why that separation is load-bearing:
4-implementation.md § 5.
REPO_ROOT=$(/usr/bin/env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE -u GIT_OBJECT_DIRECTORY -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_NAMESPACE -u GIT_CEILING_DIRECTORIES -u GIT_GLOB_PATHSPECS -u GIT_ICASE_PATHSPECS -u GIT_NOGLOB_PATHSPECS -u GIT_LITERAL_PATHSPECS -u GIT_CONFIG -u GIT_CONFIG_PARAMETERS -u GIT_CONFIG_COUNT -u GIT_CONFIG_NOSYSTEM -u GIT_CONFIG_GLOBAL -u GIT_CONFIG_SYSTEM -u GIT_IMPLICIT_WORK_TREE -u GIT_GRAFT_FILE -u GIT_SHALLOW_FILE -u GIT_PREFIX -u GIT_NO_REPLACE_OBJECTS -u GIT_REPLACE_REF_BASE -u GIT_EXTERNAL_DIFF -u ALLOW_AI_COAUTHOR git rev-parse --show-toplevel && printf .) || { echo "⚠️ could not resolve the repository root — aborting" >&2; exit 1; }
REPO_ROOT=${REPO_ROOT%.}; REPO_ROOT=${REPO_ROOT%$'\n'}
INSPECT="$REPO_ROOT/.claude/scripts/smart-commit-inspect.sh"
[ -r "$INSPECT" ] || INSPECT="$REPO_ROOT/skills/smart-commit/scripts/smart-commit-inspect.sh"
[ -r "$INSPECT" ] || { echo "⚠️ smart-commit-inspect.sh not found — run /install-scripts" >&2; exit 1; }
/bin/bash -p -- "$INSPECT" identity
Read the output by key and kind, never by line number. Field 1 is the key, field 2 is the
kind — value, empty or unset:
| Kind | Line shape |
|---|---|
value | <key><TAB>value<TAB><scope><TAB><origin><TAB><value> |
empty | <key><TAB>empty<TAB><scope><TAB><origin> — the key is set to the empty string |
unset | <key><TAB>unset |
The kind is written by the script, never read out of the config, and every data field is
escaped (\\, \n, \t, \r, and every other C0 control byte plus DEL as \xHH — P8v) —
the value, the origin path, the four env values below, and the effective ident (P8t). So one
record is always exactly one line with a fixed field count, and no data can forge a key or a
kind. Both properties are load-bearing: without them a user.name containing a newline
emits a second line reading user.name<TAB>unset and the HALT below fires on a repository that
commits perfectly well; a GIT_AUTHOR_NAME containing one forges a whole identity record; and a
GIT_AUTHOR_NAME containing a raw ESC byte carries an ANSI/terminal escape sequence straight
into whatever prints the record later.
Records for a key arrive in git's own precedence order, so the LAST one is what git would use.
Read that line, not the presence of any line — an empty value in ~/.gitconfig overridden by a
real one in .git/config produces both an empty and a value line, and only the last of them
describes the commit that would be made.
The script then prints the four env lines, and finally the four lines that carry the
verdict — two effective, two configured. On the abort path it exits before reaching any
of them.
The env lines carry the same key/kind shape (minus scope and origin, which the environment does not have), because exported-and-empty and not-exported are different states and one blank line cannot say which:
GIT_AUTHOR_NAME<TAB>value<TAB><value> # exported, non-empty — this is what the commit uses
GIT_AUTHOR_NAME<TAB>empty # exported and empty — NOT the same as unset
GIT_AUTHOR_NAME<TAB>unset # not in the environment; config decides this role
The last four lines are the answer; everything above them says where it came from.
effective<TAB>author<TAB>value<TAB><ident> # what `git commit` would record for this role
effective<TAB>committer<TAB>unresolvable # git refuses this role outright
configured<TAB>author<TAB>yes # the ident came from something set for THIS repo
configured<TAB>committer<TAB>no # it fell through to $EMAIL or git's OS guess
effective is git var GIT_AUTHOR_IDENT / GIT_COMMITTER_IDENT, asked of git rather than rebuilt
here. git applies the whole chain — GIT_<role>_* when exported, else user.*, else EMAIL, else
a guess from the OS — then runs its ident parser and refuses in exactly the cases git commit
refuses. Two consequences a table over the config rows could not reach, both measured (§ 10.6):
| Input | The records above | effective |
|---|---|---|
user.name = " " (spaces) | value — looks configured | unresolvable; git commit fatals name consists only of disallowed characters |
GIT_AUTHOR_NAME=" Alice <evil@x> " | reported verbatim | Alice evil@x <…> — git's parser strips the angle brackets, and the commit records the stripped form |
user.email unset, EMAIL exported | user.email<TAB>unset | the EMAIL value — a source no record here reports, because it is git's fallback, not a git config |
The trailing <unix-seconds> <tz> git appends is stripped: the question is who, not when.
configured is git -c user.useConfigOnly=true var GIT_<ROLE>_IDENT, asked the same way as
effective — not a re-derivation from the records above. git's useConfigOnly mode refuses
the whole fallback half of resolution (EMAIL, the OS guess) and succeeds only when the ident came
from something set for this repository or this invocation: user.*/author.*/committer.*
config, or an exported GIT_<role>_NAME/GIT_<role>_EMAIL. It exists because effective alone
cannot make this distinction — git's OS guess resolves successfully, so a value line looks
identical whether it came from .git/config or from uname. Measured: configured reads no for
both EMAIL and a bare OS guess alike, and that grouping is not a gap — from the operator's view
neither is a value they set for this repository, and git config --local fixes both the same way.
Oracles: P8r, P8s.
Decision logic — read the effective lines first, then use the records to locate the cause:
| Condition | Behavior |
|---|---|
Both lines are effective<TAB><role><TAB>value<TAB><ident>, each with a non-empty name and a non-empty address, and the matching configured<TAB><role> line reads yes | Continue with no prompt. Show that ident in the commit plan; it is what git will record, whatever combination of env, config and fallback produced it |
Either line is effective<TAB><role><TAB>unresolvable | HALT — git refuses that role, so no commit is possible. Locate the cause in the records above, and print git var GIT_AUTHOR_IDENT (or GIT_COMMITTER_IDENT) as the command that shows git's own message |
An effective ident whose address is empty — Name <> | HALT — nothing fails here; git commits and the commit simply carries no attribution, which is the outcome CLAUDE.md rule 3 exists to prevent. Measured P8m: an empty GIT_AUTHOR_EMAIL/GIT_COMMITTER_EMAIL or an empty user.email both land here |
Provenance: an effective line resolved to a value (not already caught by the empty-address row above), and the matching configured<TAB><role> line reads no | HALT — the ident did not come from anything set for this repository: it is $EMAIL or git's OS guess, and configured cannot tell those two apart (§ 10.8) — nor does the fix need it to: git config --local user.name "..." / user.email "..." (or the author.*/committer.* equivalent) covers both. This is a policy halt, not a git refusal: git commits happily on either source, user.useConfigOnly=true is what draws the line |
Two or more value records for the same config key with different values | AskUserQuestion: list candidate profiles, user selects once. effective already says which one git picked — the prompt confirms that is the intended one |
Conflict (the row above) + a CI<TAB>value<TAB>... record | HALT (fail-closed) — output fix guidance, do not silently inherit. identity emits this record the same way it emits the GIT_AUTHOR_*/GIT_COMMITTER_* ones, so the condition is readable from the fallback path's own output rather than assumed (P8t2) |
identity: could not read … on stderr, non-zero exit | Stop and report the stderr. The config was unreadable; this is not an unset key and the setup guidance above would be the wrong instruction. Any stdout printed before the abort is not an answer |
Locating the cause. None of these rows is a verdict on its own — the verdict is above. They say
which input produced it, and therefore which fix can reach it. An env line outranks config for the
role it names: git takes GIT_AUTHOR_NAME when exported and falls back to user.name only when
it is not, so a role resolved from the environment is never fixed by git config.
The same outranking happens one level down, inside config itself: author.*/committer.* outrank
user.* for their own role when both are set (git >= 2.31) — author.name wins for the author
role even if user.name is also configured, and symmetrically for committer.*. Both keys can show
an equally-valid value record below; the records alone do not say which file git actually used.
effective does — it is the row that says which config key's file to edit, not the mere presence of
a value record for user.*.
| Record | What it means for the role |
|---|---|
GIT_AUTHOR_NAME<TAB>value<TAB><value> | Exported and non-empty — this is the input; git config cannot reach it, unset can |
GIT_AUTHOR_NAME<TAB>empty | Exported and empty. For a NAME this is why effective says unresolvable; for an EMAIL it is why the address is <>. Fixed by unset, never by git config |
GIT_AUTHOR_NAME<TAB>unset | Not in the environment — config decides this role, so read its key below |
<key><TAB>value<TAB><scope><TAB><origin><TAB><value> | The configured input, and <origin> is the file to edit. Only the last record for the key is the one git used |
<key><TAB>empty<TAB><scope><TAB><origin> | Set to the empty string in <origin>. As the last record for the key this is a cause; earlier in the list it is shadowed and irrelevant |
<key><TAB>unset | Not configured anywhere. If no env var covers the role either, effective is one of two measured outcomes: it resolved from EMAIL or git's OS guess (the provenance HALT above), or it reads unresolvable — configuring the OTHER role's author.*/committer.* alone is enough to disable the OS guess for THIS one, even though this role itself has no record above saying so (P8s, § 10.8) |
Oracles: P8, P8b–P8u; the measured forgeries and misreads:
4-implementation.md § 10.
Design principles:
- Diagnostic, not override: Do not use
git -c user.name=...to override. RespectincludeIfsettings. - Ask git, do not model git: the verdict comes from
git var; the records exist to locate a cause, never to re-derive the answer. - Interrupt only on anomaly: Normal identity resolution produces no prompt.
- Conflict ≠ multiple sources:
includeIfproducing multiple config sources that resolve to the same value = normal.
1d. Signing Diagnostics
REPO_ROOT=$(/usr/bin/env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE -u GIT_OBJECT_DIRECTORY -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_NAMESPACE -u GIT_CEILING_DIRECTORIES -u GIT_GLOB_PATHSPECS -u GIT_ICASE_PATHSPECS -u GIT_NOGLOB_PATHSPECS -u GIT_LITERAL_PATHSPECS -u GIT_CONFIG -u GIT_CONFIG_PARAMETERS -u GIT_CONFIG_COUNT -u GIT_CONFIG_NOSYSTEM -u GIT_CONFIG_GLOBAL -u GIT_CONFIG_SYSTEM -u GIT_IMPLICIT_WORK_TREE -u GIT_GRAFT_FILE -u GIT_SHALLOW_FILE -u GIT_PREFIX -u GIT_NO_REPLACE_OBJECTS -u GIT_REPLACE_REF_BASE -u GIT_EXTERNAL_DIFF -u ALLOW_AI_COAUTHOR git rev-parse --show-toplevel && printf .) || { echo "⚠️ could not resolve the repository root — aborting" >&2; exit 1; }
REPO_ROOT=${REPO_ROOT%.}; REPO_ROOT=${REPO_ROOT%$'\n'}
INSPECT="$REPO_ROOT/.claude/scripts/smart-commit-inspect.sh"
[ -r "$INSPECT" ] || INSPECT="$REPO_ROOT/skills/smart-commit/scripts/smart-commit-inspect.sh"
[ -r "$INSPECT" ] || { echo "⚠️ smart-commit-inspect.sh not found — run /install-scripts" >&2; exit 1; }
/bin/bash -p -- "$INSPECT" signing
Decision logic:
signing answers in the same shape as Step 1c — <key><TAB><kind>[…] for
commit.gpgsign, user.signingkey and gpg.format, in that order, read by key and never by
line number, with the last record for a key winning. It used to print three bare lines, which a
multi-line value silently shifted (§ 10.2). Oracles: P9, P9b, P9c.
commit.gpgsign is read as a boolean, so its value field is only ever the literal true or
false: git normalises 1/yes/on/True, and a valueless gpgsign — which git reads as
true — is no longer byte-identical to gpgsign =, which git reads as false. That
distinction cannot be recovered from the raw string, which is why it is git that converts and not
a row in this table. commit.gpgsign<TAB>empty is therefore unreachable, and a non-boolean value
is a read failure rather than a verdict. Oracles: P9d–P9f; measurements: § 10.3.
| Condition (per key, last record) | Behavior |
|---|---|
commit.gpgsign<TAB>value…true + user.signingkey<TAB>value | Display Signing: enabled (<gpg.format>) |
commit.gpgsign<TAB>value…true + user.signingkey<TAB>unset or user.signingkey<TAB>empty | ⚠️ Warning: signing enabled but key not configured |
commit.gpgsign<TAB>value…false | Display Signing: disabled (commit.gpgsign=false). Distinct from the row below: this is an explicit off, so --sign contradicts a stated intent rather than an absent one |
commit.gpgsign<TAB>unset | Display Signing: not configured (inherit) |
commit.gpgsign<TAB>empty | Unreachable under --type=bool. Treat as a read failure, not as false — reaching it means the script was invoked without the flag, so the verdict would be untrustworthy either way |
gpg.format<TAB>value | Use it as the <gpg.format> shown above |
gpg.format<TAB>unset or gpg.format<TAB>empty | Use git's default, gpg. The script reports the fact; applying the default is this step's job |
signing: could not read … on stderr, non-zero exit | Stop and report the stderr — same reasoning as Step 1c. This is also where a non-boolean commit.gpgsign lands, and it is not stricter than git: git's own --bool read exits 128 on the same config, so a git commit would fail too |
--execute mode signing failure | Immediate stop + fix guidance |
Post-commit visibility (--execute mode):
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 188
- Forks
- 24
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
smart-commit-sd0xdev- Source
- github.com/sd0xdev/sd0x-harness