Feature Specification: Pre-Commit Hooks & Commit Convention Enforcement¶
Feature Branch: 002-pre-commit-hooks
Created: 2026-06-10
Status: Draft
Input: User description: "lets think about local tooling to support more controlled development flow. can we setup pre-commit hooks to auto run linting and already harden for conventional commits?"
User Scenarios & Testing (mandatory)¶
User Story 1 — Blocked commit when linting fails (Priority: P1)¶
A developer makes a change and attempts to commit. If the staged code violates any lint rule, the commit is rejected immediately with a clear message pointing to the failing file and rule. The developer fixes the issue and retries; the commit succeeds.
Why this priority: Catches lint failures at the earliest possible moment — before they reach CI or a shared branch — eliminating the "oops, fix lint" noise commits that pollute history.
Independent Test: Can be fully tested by staging a file that triggers a known ktlint or
detekt violation and running git commit. The hook must reject the commit and print the
offending location.
Acceptance Scenarios:
- Given a staged Kotlin file with a ktlint violation, When the developer runs
git commit, Then the commit is rejected and the offending file and rule are printed to the terminal. - Given a staged file with a detekt issue (e.g., complexity violation), When the developer runs
git commit, Then the commit is rejected with the detekt finding. - Given all staged files pass lint, When the developer runs
git commit, Then the commit proceeds normally without extra delay. - Given a developer needs to bypass the hook for an emergency, When they use the standard Git bypass flag (
--no-verify), Then the commit proceeds with a visible warning in the output.
User Story 2 — Blocked commit on invalid commit message format (Priority: P1)¶
A developer types a free-form commit message. The commit-msg hook validates it against the
Conventional Commits format (type(scope): description). If the message does not match, the
commit is rejected with an example of the correct format. The developer amends the message and
the commit succeeds.
Why this priority: Enables automated changelog generation, semantic release, and consistent
git log readability without relying on reviewer memory. Best enforced at commit time rather
than in review.
Independent Test: Can be fully tested by attempting a commit with message "fixed stuff".
The hook must reject it and show an example like "fix(auth): handle null user on login".
Acceptance Scenarios:
- Given a commit message
"fixed stuff", When the developer commits, Then the commit is rejected and the terminal shows the allowedtypevalues and an example. - Given a commit message
"feat(api): add document submission endpoint", When the developer commits, Then the commit proceeds. - Given a commit message
"feat!: breaking change to task API"(breaking-change notation), When the developer commits, Then the commit proceeds. - Given a WIP/merge/revert commit, When the developer commits, Then the hook accepts messages starting with
Merge,Revert, orWIPwithout formatting requirements.
User Story 3 — Zero-configuration onboarding for new contributors (Priority: P2)¶
A new contributor clones the repository and runs a single documented command to install the hooks locally. From that point on, all pre-commit and commit-msg checks run automatically with no per-machine configuration.
Why this priority: Local validation (Constitution Principle VI) only works if the hooks are actually installed. If installation requires manual steps that are easily missed, contributors skip them.
Independent Test: Can be fully tested by cloning the repo into a fresh directory, running the documented install command, and verifying that a lint-failing commit is rejected.
Acceptance Scenarios:
- Given a fresh clone with no prior setup, When the contributor runs the documented install command, Then both the pre-commit and commit-msg hooks are active.
- Given hooks are installed, When the contributor updates the repo (e.g.,
git pull), Then the hooks remain active without re-installation. - Given the install command is run a second time, When it completes, Then existing hooks are not duplicated or corrupted.
Edge Cases¶
- What happens when only non-Kotlin files are staged (e.g., a YAML or SQL file)? Lint should be skipped or scoped to avoid false positives on irrelevant file types.
- How does the commit-msg hook behave for automated commits (CI bots,
release-please, merge commits generated by GitHub)? These should be exempted or match an explicit allowlist. - What happens when the lint tooling itself is not available locally (e.g., Gradle wrapper
has not been set up)? The hook must fail gracefully with a clear "run
./gradlew buildfirst" message rather than a cryptic error.
Requirements (mandatory)¶
Functional Requirements¶
- FR-001: The repository MUST include a pre-commit hook that runs lint checks (ktlint + detekt) on staged Kotlin files before each commit.
- FR-002: Lint failures MUST block the commit and print the offending file path, line number, and rule name to the terminal.
- FR-003: The repository MUST include a commit-msg hook that validates commit messages against the Conventional Commits v1.0 specification.
- FR-004: Invalid commit messages MUST be rejected with a human-readable error showing allowed types and a correct example.
- FR-005: Both hooks MUST be installable via a single documented command (e.g.,
./gradlew installGitHooksor equivalent). - FR-006: The installation command MUST be idempotent (safe to run multiple times).
- FR-007: Hooks MUST be version-controlled so all contributors use the same rules.
- FR-008: Hooks MUST respect the standard
--no-verifybypass without silent failures; bypass usage SHOULD print a warning to the terminal. - FR-009: Lint checks in the pre-commit hook MUST run only on staged Kotlin files to keep commit latency acceptable.
- FR-010: Merge commits, revert commits, and WIP commits MUST be exempt from commit-message formatting requirements.
- FR-011: The allowed Conventional Commits types MUST be documented (e.g.,
feat,fix,docs,chore,refactor,test,ci,build,perf).
Key Entities¶
- Pre-commit hook: Script that runs lint checks on staged files before a commit is recorded.
- Commit-msg hook: Script that validates the commit message against a format rule.
- Hook installer: A task or script that writes hooks into
.git/hooks/and makes them executable, triggered by a documented developer command. - Conventional Commit: A commit message of the form
type(optional-scope): description, optionally with a breaking-change footer or!suffix.
Success Criteria (mandatory)¶
Measurable Outcomes¶
- SC-001: A commit with a ktlint or detekt violation is rejected 100% of the time; the error message identifies the file and rule within 2 seconds of the commit command.
- SC-002: A commit with a non-conformant message (e.g.,
"wip","fix stuff") is rejected 100% of the time with a visible example of the correct format. - SC-003: A new contributor can activate the hooks with one documented command; hook installation takes under 10 seconds.
- SC-004: Pre-commit lint checks on a typical staged change (1–5 Kotlin files) complete within 30 seconds, keeping the commit experience fluid.
- SC-005: Zero false positives on non-Kotlin staged files (YAML, SQL, Markdown) — those commits proceed without triggering lint.
Assumptions¶
- The project uses Git as its version-control system and the
.git/directory is at the repo root. - Contributors have Java/JDK available locally (required to run Gradle and the Kotlin linters).
- ktlint and detekt are already configured in the Gradle build (confirmed from existing
build-logic/convention plugins); the hooks will invoke them via Gradle rather than duplicating configuration. - The commit-msg hook will be implemented as a shell script or lightweight tool (not a full Node.js/npm dependency), keeping the toolchain requirement to JVM + shell only.
- Automated CI commits (e.g., from release tooling or bots) will use
--no-verifyby convention and are out of scope for enforcement here. - Mobile support and IDE-level lint integration are out of scope; this spec covers only the Git-hook layer.