Skip to content

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:

  1. 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.
  2. 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.
  3. Given all staged files pass lint, When the developer runs git commit, Then the commit proceeds normally without extra delay.
  4. 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:

  1. Given a commit message "fixed stuff", When the developer commits, Then the commit is rejected and the terminal shows the allowed type values and an example.
  2. Given a commit message "feat(api): add document submission endpoint", When the developer commits, Then the commit proceeds.
  3. Given a commit message "feat!: breaking change to task API" (breaking-change notation), When the developer commits, Then the commit proceeds.
  4. Given a WIP/merge/revert commit, When the developer commits, Then the hook accepts messages starting with Merge, Revert, or WIP without 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:

  1. 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.
  2. Given hooks are installed, When the contributor updates the repo (e.g., git pull), Then the hooks remain active without re-installation.
  3. 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 build first" 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 installGitHooks or 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-verify bypass 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-verify by 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.