linthis 0.22.0

A fast, cross-platform multi-language linter and formatter
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# 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 via `git 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

```bash
# 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

```bash
# 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:

1. Writes a hook script to `~/.config/git/hooks/pre-commit`
2. 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]https://github.com/prek-dev/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

```bash
# 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:

1. **Writes** `~/.config/git/hooks/<event>` — the hook script
2. **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)

```bash
#!/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)

```bash
#!/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`.

```toml
[hook.git]
pre-commit = { source = { plugin = "my-plugin", file = "hooks/git/pre-commit" } }
```

Five source variants are supported:

```toml
# 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:

1. Replaces `plugin = "self"` with `plugin = "<alias>"` in the bundled config
2. 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

```bash
# 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:

```bash
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 `linthis` call
- Which global hooks are installed
- The active `core.hooksPath` setting
- 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

```bash
# 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

```bash
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

```bash
# Remove the project pre-commit hook
linthis hook uninstall

# Remove the project pre-push hook
linthis hook uninstall --event pre-push
```

---

## Command Reference

```bash
# 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:

```bash
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?

```bash
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:

```toml
[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](./fix-commit-mode.md) for the full behavior matrix.

## See Also

- [Fix Commit Mode]./fix-commit-mode.md — Detailed behavior matrix for squash/dirty/fixup
- [AI-Powered Fix]./ai-fix.md — AI provider details
- [AI Coding Agent Integration]./agent-hooks.md — Rules-based agent integration
- [CLI Reference]../reference/cli.md — Complete command reference
- [Git documentation — core.hooksPath]https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath