Browse documentation
DocsWorkflows

Git hooks

Catch spec drift at commit time, before CI. Each recipe runs agnostic-ai sync --check whenever a spec or agnostic-ai.yaml is staged, blocking the commit if any generated file is out of date.

The same sync --check powers the CI gate. Running it locally shortens the feedback loop from "push, wait, fail" to "commit, fix, commit".

For ignored outputs, run sync when bootstrapping each checkout before enabling a drift hook. In CI, use the ignored-output recipe. A local drift check compares your working tree, not only staged files.

Why a pre-commit hook

  • Drift surfaces when you create it, not 30 seconds into the next CI run.
  • Same gate on every contributor and machine.
  • Opt-in per checkout, so a fresh clone still works without setup.

pre-commit (Python)

pre-commit is the common choice in polyglot repos. Add .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: agnostic-ai-check
        name: agnostic-ai sync --check
        entry: agnostic-ai sync --check
        language: system
        pass_filenames: false
        files: '^(\.agnostic-ai/|agnostic-ai\.yaml$|agnostic\.config\.yaml$)'

Install once per checkout:

pre-commit install

files: scopes the hook to spec changes; unrelated commits skip the check. pass_filenames: false runs the binary on the whole project (like CI) instead of passing each staged path.

lefthook

lefthook is a single Go binary, no runtime dependency. This repo dogfoods it; see lefthook.yml.

Add to lefthook.yml:

pre-commit:
  commands:
    agnostic-ai-check:
      glob: "{.agnostic-ai/**,agnostic-ai.yaml}"
      run: agnostic-ai sync --check

Install once per checkout:

lefthook install

glob: keeps the hook silent unless a spec or the config is staged.

husky + lint-staged

In Node projects, husky plus lint-staged is the standard pairing.

package.json:

{
  "scripts": {
    "prepare": "husky"
  },
  "lint-staged": {
    "{.agnostic-ai/**,agnostic-ai.yaml}": "agnostic-ai sync --check --"
  }
}

.husky/pre-commit:

npx lint-staged

Install once per checkout:

npm install

The trailing -- swallows the staged paths lint-staged appends; sync --check reads the project root, not individual files.

Regenerate on checkout

The hooks above catch drift at commit time. They do not help when generated outputs are gitignored (gitignore.enabled: true): a fresh clone or a new git worktree then starts with no CLAUDE.md, rules, or hooks until someone runs sync. A contributor cloning the repo runs sync by hand; automated worktree creation does not, so an AI session opened there finds no config.

A post-checkout hook closes the gap. git checkout, git clone, and git worktree add all fire it, so outputs regenerate themselves.

lefthook (lefthook.yml):

post-checkout:
  commands:
    sync:
      run: agnostic-ai sync

Plain git (.git/hooks/post-checkout, chmod +x):

#!/bin/sh
agnostic-ai sync

post-checkout receives three arguments; a file checkout passes 0 as the third. Guard on it if you only want the hook on branch and worktree switches:

#!/bin/sh
[ "$3" = "1" ] || exit 0   # 1 = branch checkout, 0 = file checkout
agnostic-ai sync

This needs agnostic-ai on PATH in every environment that checks out the repo. If contributors may lack the CLI, commit the generated outputs instead of gitignoring them.

Tips

  • The hook needs agnostic-ai on PATH. Document the install in CONTRIBUTING.md so new contributors avoid command not found on their first commit.
  • To recover from drift, run agnostic-ai sync and stage the regenerated outputs alongside the spec change.
  • Set gitignore.enabled: true in agnostic-ai.yaml to keep generated outputs out of git. The hook still catches drift because sync --check ignores gitignore.
  • Skip a hook for one commit with git commit --no-verify. Save it for emergencies.