Git Hooks¶
Overview¶
linthis integrates with Git's hook system to run lint checks and formatting automatically at commit time (or push time). Hooks can be installed at two scopes:
- Project-level — written to
.git/hooks/<event>in a single repository - Global — written to
~/.config/git/hooks/<event>and activated for every repository on the machine viagit config --global core.hooksPath
Global hooks use a local-priority strategy: a local project hook takes priority. If the local .git/hooks/<event> already calls linthis, the global hook delegates to it entirely. If a local hook exists but does not call linthis, the global hook runs linthis first and then chains to the local hook. If there is no local hook at all, the global hook runs linthis directly. This design guarantees zero interference with other hook tools.
All hook types — git, prek, git-with-agent, prek-with-agent — are supported at both scopes.
Quick Start¶
Project-level hooks¶
# Default: git pre-commit hook
linthis hook install
# Git pre-push hook
linthis hook install --event pre-push
# Commit message format hook
linthis hook install --event commit-msg
# prek hook (for projects using prek)
linthis hook install --type prek
Global hooks¶
# Global git pre-commit hook (applied to every repository)
linthis hook install --global
# Global git pre-push hook
linthis hook install --global --event pre-push
# Global hook, non-interactive
linthis hook install --global -y
After running linthis hook install --global, the command:
- Writes a hook script to
~/.config/git/hooks/pre-commit - Runs
git config --global core.hooksPath ~/.config/git/hooks
Every repository on the machine will now run that hook. No git init re-run is required for existing repositories.
Hook Types¶
| Type | Runner | Trigger | Notes |
|---|---|---|---|
git |
Git native | .git/hooks/<event> |
Default type; no extra tooling required |
prek |
prek | prek's runner | Requires prek installed; config committed to repo |
git-with-agent |
Git native | .git/hooks/<event> |
Same as git, plus AI agent fix fallback on lint failure |
prek-with-agent |
prek | prek's runner | Same as prek, plus AI agent fix fallback |
Global Hooks¶
Installing¶
# Install global pre-commit hook (git type)
linthis hook install --global
# Install global pre-push hook
linthis hook install --global --event pre-push
# Install global hook with agent fix fallback
linthis hook install --global --type git-with-agent --provider claude
# Non-interactive (skip confirmation prompts)
linthis hook install --global -y
How it works¶
--global performs two actions:
- Writes
~/.config/git/hooks/<event>— the hook script - Sets
git config --global core.hooksPath ~/.config/git/hooks
Git's core.hooksPath makes Git look in that directory for all hooks, for every repository, immediately — no per-repo setup needed.
Directory layout¶
~/.config/git/hooks/
├── pre-commit # installed by linthis hook install --global
├── pre-push # installed by linthis hook install --global --event pre-push
└── ...
Strategy — local-priority delegation¶
The global hook does not run blindly. Before running linthis, it inspects the local .git/hooks/<event> of the current repository:
| Local hook state | Global hook behaviour |
|---|---|
| No local hook | Runs linthis directly |
Local hook exists, does not call linthis |
Runs linthis first, then delegates to the local hook |
Local hook exists, calls linthis |
Delegates entirely (exec "$LOCAL_HOOK" "$@") — linthis is not double-run |
Detection uses grep -qE '^[^#]*linthis' — it matches any non-comment line containing linthis, so renaming comments does not affect the result.
Generated global hook script (commit-msg, git type)¶
#!/bin/sh
# linthis-hook
LINTHIS_CMD="linthis cmsg"
# Locate the local project hook (git-dir aware)
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null)"
LOCAL_HOOK=""
if [ -n "$GIT_DIR" ]; then
LOCAL_HOOK="$GIT_DIR/hooks/commit-msg"
fi
if [ -f "$LOCAL_HOOK" ] && [ -x "$LOCAL_HOOK" ]; then
if grep -qE '^[^#]*linthis' "$LOCAL_HOOK" 2>/dev/null; then
# Local hook already calls linthis — delegate entirely
exec "$LOCAL_HOOK" "$@"
else
# Local hook exists but has no linthis — run linthis first, then delegate
$LINTHIS_CMD "$@"
LINTHIS_EXIT=$?
"$LOCAL_HOOK" "$@"
LOCAL_EXIT=$?
[ $LINTHIS_EXIT -ne 0 ] && exit $LINTHIS_EXIT
exit $LOCAL_EXIT
fi
else
# No local hook — run linthis directly
$LINTHIS_CMD "$@"
LINTHIS_EXIT=$?
exit $LINTHIS_EXIT
fi
Note: $@ passes git's $1 (the message file path) safely, even for paths with spaces.
Generated global hook script (pre-commit, git type)¶
#!/bin/sh
# linthis-hook
LINTHIS_CMD="linthis -s -c -f --hook-event=pre-commit"
# Locate the local project hook (git-dir aware)
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null)"
LOCAL_HOOK=""
if [ -n "$GIT_DIR" ]; then
LOCAL_HOOK="$GIT_DIR/hooks/pre-commit"
fi
if [ -f "$LOCAL_HOOK" ] && [ -x "$LOCAL_HOOK" ]; then
if grep -qE '^[^#]*linthis' "$LOCAL_HOOK" 2>/dev/null; then
# Local hook already calls linthis — delegate entirely
exec "$LOCAL_HOOK" "$@"
else
# Local hook exists but has no linthis — run linthis first, then delegate
$LINTHIS_CMD
LINTHIS_EXIT=$?
"$LOCAL_HOOK" "$@"
LOCAL_EXIT=$?
[ $LINTHIS_EXIT -ne 0 ] && exit $LINTHIS_EXIT
exit $LOCAL_EXIT
fi
else
# No local hook — run linthis directly
$LINTHIS_CMD
LINTHIS_EXIT=$?
exit $LINTHIS_EXIT
fi
Three-Tier Hook Resolution¶
When linthis hook install runs, it resolves the hook script through three tiers (highest → lowest priority):
| Tier | Source | How to use |
|---|---|---|
| Tier 1 | Fixed-path auto-discovery | Place a script at hooks/git/<event> in your project root |
| Tier 2 | TOML source mapping | Set [hook.git] entries in .linthis/config.toml |
| Tier 3 | Built-in generator | Default — the built-in generated script |
Tier 1: Fixed-Path Auto-Discovery¶
Create an executable file at the conventional path relative to your project root:
hooks/git/pre-commit
hooks/git/pre-push
hooks/git/commit-msg
If this file exists, linthis uses it directly without generating its own script. No config needed.
Tier 2: TOML Source Mapping¶
Override the hook source in .linthis/config.toml using a source entry. Plugins typically inject these entries automatically when added via linthis plugin add.
[hook.git]
pre-commit = { source = { plugin = "my-plugin", file = "hooks/git/pre-commit" } }
Five source variants are supported:
# Local file (relative to project root)
pre-commit = { source = { file = "hooks/git/pre-commit" } }
# File inside an installed plugin
pre-commit = { source = { plugin = "my-plugin", file = "hooks/git/pre-commit" } }
# File from a marketplace plugin
pre-commit = { source = { marketplace = "corp", plugin = "linthis-official", file = "hooks/git/pre-commit" } }
# Direct URL download
pre-commit = { source = { url = "https://example.com/hooks/pre-commit" } }
# Clone a git repo
pre-commit = { source = { git = "https://github.com/org/hooks.git", ref = "main", path = "pre-commit" } }
The same override structure applies to all hook types ([hook.git-with-agent], [hook.prek], [hook.prek-with-agent], etc.).
Plugin-Bundled Hooks¶
Plugins can bundle hook overrides inside a linthis-hook.toml at the plugin root. When a user runs linthis plugin add <alias> <url>, linthis automatically:
- Replaces
plugin = "self"withplugin = "<alias>"in the bundled config - Non-overwritingly merges
[hook.*]entries into the user's.linthis/config.toml
This means adding a team plugin is all it takes for everyone to get the team's custom pre-commit scripts automatically.
*-with-agent Hook Types¶
The git-with-agent and prek-with-agent types add an AI agent fix fallback. When linthis exits with a non-zero status (lint failure), the hook invokes the chosen agent CLI in headless mode to attempt an automatic fix, then re-runs linthis to verify the result.
Supported providers¶
--provider value |
Agent CLI | Headless command |
|---|---|---|
claude |
Claude Code CLI | claude -p '<prompt>' |
codex |
OpenAI Codex CLI | codex exec '<prompt>' |
gemini |
Google Gemini CLI | gemini -p '<prompt>' |
cursor |
Cursor agent | cursor-agent chat '<prompt>' |
droid |
Droid | droid exec --auto low '<prompt>' |
auggie |
Auggie | auggie --print '<prompt>' |
The --provider flag supports provider/model syntax (e.g. claude/opus) which is equivalent to --provider claude --provider-args "--model opus". Use --provider-args to pass additional arguments to the AI agent CLI.
Examples¶
# Project-level: git hook with Claude fix fallback
linthis hook install --type git-with-agent --provider claude
# Project-level: prek hook with Gemini fix fallback
linthis hook install --type prek-with-agent --provider gemini
# With provider/model syntax (passes --model to agent CLI)
linthis hook install --type git-with-agent --provider claude/opus
# With explicit provider-args
linthis hook install --type git-with-agent --provider claude --provider-args "--model opus"
# Global: git hook with Claude fix fallback
linthis hook install --global --type git-with-agent --provider claude
hook status¶
Check the current state of all installed hooks:
linthis hook status
Example output:
Git Hook Status
Repository: /path/to/repo
Project Hooks (.git/hooks/):
✓ /path/.git/hooks/pre-commit [project]
pre-commit (runs before commit)
✓ linthis
Global Hooks (~/.config/git/hooks/):
core.hooksPath = /Users/username/.config/git/hooks
✓ /Users/username/.config/git/hooks/pre-commit [global]
ℹ Strategy: local hook takes priority
The status output shows:
- Which project-level hooks are installed and whether they contain a
linthiscall - Which global hooks are installed
- The active
core.hooksPathsetting - The delegation strategy in use
Global vs Project Comparison¶
| Feature | Global (--global) |
Project-level |
|---|---|---|
| Scope | Every repository on the machine | Current repository only |
| Location | ~/.config/git/hooks/ |
.git/hooks/ |
| Git config changed | core.hooksPath (global) |
None |
| Works for existing repos | Yes, immediately | Yes, immediately |
| Committable to repo | No | No (.git/ is not tracked) |
| Team sharing | No | Requires prek or pre-commit type |
| Hook coexistence | Local-priority (auto-delegation) | Manual chaining |
| Supported types | All types | All types |
Uninstall¶
Remove a specific global hook¶
# Remove global pre-commit hook
linthis hook uninstall --global
# Remove global pre-push hook
linthis hook uninstall --global --event pre-push
# Non-interactive
linthis hook uninstall --global -y
Remove all global hooks¶
linthis hook uninstall --global --all
# Non-interactive
linthis hook uninstall --global --all -y
--all removes all hook scripts from ~/.config/git/hooks/ and unsets core.hooksPath if no other hooks remain.
Remove a project-level hook¶
# Remove the project pre-commit hook
linthis hook uninstall
# Remove the project pre-push hook
linthis hook uninstall --event pre-push
Command Reference¶
# Project-level install
linthis hook install # git pre-commit
linthis hook install --event pre-push # git pre-push
linthis hook install --type prek # prek
linthis hook install --type git-with-agent --provider claude # git + agent fix
linthis hook install --type git-with-agent --provider claude/opus # git + agent fix (with model)
linthis hook install --type prek-with-agent --provider gemini # prek + agent fix
# Global install
linthis hook install --global # global git pre-commit
linthis hook install --global --event pre-push # global git pre-push
linthis hook install --global --type git-with-agent --provider claude # global + agent fix
linthis hook install --global -y # non-interactive
# Uninstall
linthis hook uninstall # remove project pre-commit
linthis hook uninstall --global # remove global pre-commit
linthis hook uninstall --global --all # remove all global hooks
linthis hook uninstall --global -y # non-interactive
# Status
linthis hook status
FAQ¶
Q1: Can a global hook and a project-level hook coexist?¶
Yes. This is the local-priority strategy's primary use case. If the project has a .git/hooks/pre-commit that calls linthis, the global hook detects it and delegates entirely — linthis runs once, not twice. If the project hook does not call linthis, the global hook prepends linthis before calling the project hook.
Q2: How does the local-priority strategy detect whether the local hook calls linthis?¶
It runs grep -qE '^[^#]*linthis' "$LOCAL_HOOK". The pattern matches any non-comment line (^[^#]*) that contains the string linthis. Comment lines starting with # are ignored. This means renaming a comment or adding a note like # previously used linthis does not affect detection — only executable lines matter.
Q3: How do I disable the global hook for a specific repository?¶
Install a project-level hook that calls linthis. The global hook will detect it and delegate, so the project hook is the sole entry point. You then have full control over how linthis is invoked in that repository.
Alternatively, install any project-level hook that does not call linthis. The global hook will still run linthis before it — to suppress that, remove the global hook for that event or use --event to choose a different event scope.
If you want linthis to be completely silent in one repository, create a no-op project hook:
printf '#!/bin/sh\n# intentionally no linthis\nexit 0\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
The global hook will see that this hook does not call linthis, so it will run linthis first. To suppress linthis entirely in that repo, the cleanest approach is to uninstall the global hook and rely on project-level hooks only.
Q4: Will the global hook affect repositories that do not use linthis?¶
The hook will attempt to run linthis -s -c -f --hook-event=pre-commit. If the repository has no linthis configuration (.linthis/config.toml, .linthis.toml, or linthis.toml), linthis exits immediately with no errors. The commit proceeds normally.
Q5: What happens if the agent CLI is not installed but I used --type git-with-agent?¶
The hook first runs linthis. If linthis exits cleanly, the agent is never invoked. If linthis fails and the agent CLI binary is missing, the hook prints a warning and exits with the original linthis exit code so the commit is still blocked.
Q6: Can I use --type prek or --type pre-commit with --global?¶
Yes. All hook types are supported with --global. The hook script written to ~/.config/git/hooks/<event> will invoke the appropriate runner (prek or pre-commit) rather than linthis directly. The same local-priority delegation logic applies.
Q7: How do I check which core.hooksPath is active?¶
git config --global --get core.hooksPath
# Output: /Users/username/.config/git/hooks
If this returns nothing, no global core.hooksPath is set and Git is using .git/hooks/ per-repository as usual.
Fix Commit Mode¶
Controls how auto-format and agent fix changes are committed. Configure per event:
[hook.pre_commit]
fix_commit_mode = "squash" # squash | dirty | fixup
[hook.pre_push]
fix_commit_mode = "dirty" # squash | dirty | fixup
Or set via CLI: linthis hook install --fix-commit-mode <mode>
| Mode | Behavior |
|---|---|
| squash | Fix + create fixup commit + squash into original. Stash snapshot preserved. |
| dirty | Fix + leave in working tree + block commit/push. User reviews first. |
| fixup | Let original commit through. Post-commit creates a separate fixup commit. |
See Fix Commit Mode for the full behavior matrix.
See Also¶
- Fix Commit Mode — Detailed behavior matrix for squash/dirty/fixup
- AI-Powered Fix — AI provider details
- AI Coding Agent Integration — Rules-based agent integration
- CLI Reference — Complete command reference
- Git documentation — core.hooksPath