claude-git-hooks 2.66.1 → 2.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * File: skill-registry/runner.js
3
+ * Purpose: Run the parsed skill-registry's Verify patterns against a set of
4
+ * staged files. Returns structured findings the pre-commit hook
5
+ * can surface alongside (or instead of) AI analysis.
6
+ *
7
+ * Design:
8
+ * - Inputs: array of rules from parser.js + array of staged absolute file paths.
9
+ * - For each staged file:
10
+ * 1. Read its content.
11
+ * 2. Pick the rules whose scope matches the file's language (backend
12
+ * rules for .java, frontend for .jsx/.tsx/.js/.css, SQL rules for
13
+ * .sql — eventually; for now we map by scope tag).
14
+ * 3. For each rule with an extractable regex from its Verify command,
15
+ * search the file and emit findings with line numbers.
16
+ * - Returns: { totalFindings, byFile: { path → [findings] }, bySeverity: {…} }
17
+ *
18
+ * NOT done here:
19
+ * - File-level greps (e.g. "files that DON'T contain X" / inverted verifies):
20
+ * those need a different model (whole-repo audit, not pre-commit).
21
+ * - Cross-file dependencies (verifies that pipe through xargs).
22
+ * - The deny-list / allow-list around which rules block vs warn — caller
23
+ * decides via the severity field on each rule.
24
+ */
25
+
26
+ import { readFileSync } from 'fs';
27
+ import { extname } from 'path';
28
+ import { SCOPE_BY_EXT } from './parser.js';
29
+
30
+ /**
31
+ * Run all applicable rules against a set of staged files.
32
+ * @param {Array<object>} rules - From parser.loadRegistry().rules
33
+ * @param {Array<string>} stagedAbsPaths - Absolute file paths
34
+ * @param {object} [options]
35
+ * @param {string} [options.repoRoot] - For displaying relative paths in output
36
+ * @returns {{ totalFindings: number, findings: Array, byFile: object, bySeverity: object }}
37
+ */
38
+ export function runRules(rules, stagedAbsPaths, options = {}) {
39
+ const repoRoot = options.repoRoot || '';
40
+ const findings = [];
41
+
42
+ // Precompile each rule's matchers once.
43
+ const compiled = compileRules(rules);
44
+
45
+ for (const absPath of stagedAbsPaths) {
46
+ const ext = extname(absPath).toLowerCase();
47
+ const scope = SCOPE_BY_EXT[ext];
48
+ if (!scope) continue;
49
+
50
+ // Skip binary / very large files defensively.
51
+ let content;
52
+ try {
53
+ content = readFileSync(absPath, 'utf8');
54
+ if (content.length > 1_000_000) continue; // 1MB cap
55
+ } catch {
56
+ continue;
57
+ }
58
+
59
+ const applicable = compiled.filter((r) => r.scope === scope);
60
+ if (applicable.length === 0) continue;
61
+
62
+ const lines = content.split('\n');
63
+ for (const rule of applicable) {
64
+ for (const matcher of rule.matchers) {
65
+ // Try matching each line; collect line numbers.
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i];
68
+ if (matcher.exclude && matcher.exclude.test(line)) continue;
69
+ if (matcher.regex.test(line)) {
70
+ // Reset lastIndex for global regexes; we only need the first hit per line.
71
+ matcher.regex.lastIndex = 0;
72
+ findings.push({
73
+ ruleId: rule.id,
74
+ severity: rule.severity,
75
+ title: rule.title,
76
+ scope: rule.scope,
77
+ file: repoRoot ? relativizePath(absPath, repoRoot) : absPath,
78
+ absPath,
79
+ line: i + 1,
80
+ lineText: line.trim().slice(0, 200),
81
+ // Lightweight pointer back to the registry entry for the UI.
82
+ registryRef: `${rule.scope}/improvement-registry.md → ${rule.id}`,
83
+ });
84
+ } else {
85
+ matcher.regex.lastIndex = 0;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ return summarize(findings);
93
+ }
94
+
95
+ /**
96
+ * Convert each rule's verify commands into JS RegExp matchers we can run
97
+ * line-by-line. Rules where the verify can't be extracted are skipped (they
98
+ * remain in the registry for other features like `lookup`).
99
+ */
100
+ function compileRules(rules) {
101
+ const out = [];
102
+ for (const rule of rules) {
103
+ const matchers = [];
104
+ for (const v of rule.verifies) {
105
+ const m = extractMatcher(v.command);
106
+ if (m) matchers.push(m);
107
+ }
108
+ if (matchers.length > 0) {
109
+ out.push({
110
+ id: rule.id,
111
+ severity: rule.severity,
112
+ title: rule.title,
113
+ scope: rule.scope,
114
+ matchers,
115
+ });
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+
121
+ /**
122
+ * Extract a regex + optional exclude regex from a Verify grep command.
123
+ *
124
+ * Supported shapes (covers ~50 of the 95 current registry entries):
125
+ * grep -rn "PATTERN" path
126
+ * grep -rEn "REGEX" path
127
+ * grep -rn "PATTERN" path --include=*.ext
128
+ * grep -rn "PATTERN" path | grep -v "EXCLUDE"
129
+ * grep -rEn "REGEX1" path | grep -E "REGEX2" (we treat the pipe as AND)
130
+ * rg "PATTERN" path
131
+ *
132
+ * Returns { regex, exclude } or null if the command doesn't fit the supported shapes.
133
+ */
134
+ function extractMatcher(command) {
135
+ if (!command) return null;
136
+
137
+ // Strip quoted strings before checking for shell control characters —
138
+ // a literal `;` inside the grep regex (e.g. `^import .*\.\*;`) must
139
+ // not trip our reject heuristic.
140
+ const outsideQuotes = command
141
+ .replace(/"[^"]*"/g, '"_"')
142
+ .replace(/'[^']*'/g, "'_'");
143
+
144
+ if (/xargs/.test(outsideQuotes)) return null;
145
+ // Shell control outside quotes (||, &&, ;, $(), backticks for command sub) → file-level / audit-style.
146
+ if (/\|\||&&|;|\$\(|`/.test(outsideQuotes)) return null;
147
+ if (/\s\|\s/.test(outsideQuotes) && !/\|\s+grep/.test(outsideQuotes)) return null;
148
+
149
+ // Split on the (optional) `| grep` pipe. We support: first grep matches,
150
+ // pipe-through grep further filters (positive or `-v` excludes).
151
+ const segments = command.split(/\s\|\s/).map((s) => s.trim());
152
+ const primary = segments[0];
153
+
154
+ const tokens = tokenizeGrepInvocation(primary);
155
+ if (!tokens) return null;
156
+
157
+ // Reject FILE-LEVEL grep modes (list filenames only) — those are
158
+ // whole-repo audits, not line-level pre-commit checks.
159
+ // -l → list matching filenames only
160
+ // -L → list NON-matching filenames only (inverted file presence)
161
+ // We accept `-ln`/`-rln`/`-Ln` because `n` is line numbers (still file-level).
162
+ if (tokens.flags.includes('l') || tokens.flags.includes('L')) return null;
163
+
164
+ const isExtended = tokens.flags.includes('E');
165
+ let pattern;
166
+ try {
167
+ pattern = isExtended ? tokens.pattern : escapeForRegex(tokens.pattern);
168
+ } catch {
169
+ return null;
170
+ }
171
+
172
+ let regex;
173
+ try {
174
+ regex = new RegExp(pattern);
175
+ } catch {
176
+ return null;
177
+ }
178
+
179
+ // Optional exclude from a `| grep -v "X"` follow-up.
180
+ let exclude = null;
181
+ for (const seg of segments.slice(1)) {
182
+ const t = tokenizeGrepInvocation(seg);
183
+ if (!t) continue;
184
+ if (t.flags.includes('v')) {
185
+ try {
186
+ const excPat = t.flags.includes('E') ? t.pattern : escapeForRegex(t.pattern);
187
+ exclude = new RegExp(excPat);
188
+ } catch {
189
+ /* leave exclude null */
190
+ }
191
+ }
192
+ // Non-inverted secondary grep == narrowing AND. We could AND it with
193
+ // primary but in practice the registry uses this for file-list filtering
194
+ // (e.g. JBE-004 piping into `grep -A 1`), which is whole-file context
195
+ // not single-line. Conservative: skip.
196
+ }
197
+
198
+ return { regex, exclude };
199
+ }
200
+
201
+ /**
202
+ * Tokenize a single `grep ...` invocation into { flags, pattern, paths }.
203
+ *
204
+ * The pattern is the FIRST positional argument after the flags.
205
+ * Patterns may be quoted with " or ' or backticks; flags can be combined or split.
206
+ *
207
+ * Returns null if we can't recognise the shape (in which case the rule's
208
+ * verify just doesn't fire — silently skipped).
209
+ */
210
+ function tokenizeGrepInvocation(s) {
211
+ const trimmed = s.trim();
212
+ // Must start with grep / rg / git grep
213
+ let rest;
214
+ if (/^grep\s/.test(trimmed)) rest = trimmed.slice(5).trim();
215
+ else if (/^rg\s/.test(trimmed)) rest = trimmed.slice(3).trim();
216
+ else if (/^git\s+grep\s/.test(trimmed)) rest = trimmed.replace(/^git\s+grep\s/, '').trim();
217
+ else return null;
218
+
219
+ const flags = [];
220
+ while (rest.startsWith('-')) {
221
+ // Stop at '--' (separator)
222
+ if (rest.startsWith('-- ')) {
223
+ rest = rest.slice(3).trim();
224
+ break;
225
+ }
226
+ const m = rest.match(/^-(-?)([A-Za-z]+)(=(\S+))?\s*/);
227
+ if (!m) break;
228
+ const isLong = m[1] === '-';
229
+ if (isLong) {
230
+ flags.push(m[2]);
231
+ } else {
232
+ for (const ch of m[2]) flags.push(ch);
233
+ }
234
+ rest = rest.slice(m[0].length);
235
+ }
236
+
237
+ // First positional: the pattern. Strip surrounding quotes if any.
238
+ const patMatch = rest.match(/^("([^"]*)"|'([^']*)'|(\S+))/);
239
+ if (!patMatch) return null;
240
+ const pattern = patMatch[2] ?? patMatch[3] ?? patMatch[4];
241
+
242
+ return { flags, pattern };
243
+ }
244
+
245
+ function escapeForRegex(s) {
246
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
247
+ }
248
+
249
+ function relativizePath(abs, root) {
250
+ const r = root.replace(/[\\/]+$/, '');
251
+ if (abs.startsWith(r)) {
252
+ return abs.slice(r.length + 1).replace(/\\/g, '/');
253
+ }
254
+ return abs.replace(/\\/g, '/');
255
+ }
256
+
257
+ function summarize(findings) {
258
+ const byFile = {};
259
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 };
260
+ for (const f of findings) {
261
+ (byFile[f.file] ||= []).push(f);
262
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
263
+ }
264
+ return { totalFindings: findings.length, findings, byFile, bySeverity };
265
+ }
@@ -247,15 +247,21 @@ export function discoverVersionFiles(options = {}) {
247
247
  for (const fileType of fileTypes) {
248
248
  const registry = VERSION_FILE_TYPES[fileType];
249
249
  if (registry && entry.name === registry.filename) {
250
- const version = registry.readVersion(fullPath);
251
- const isSemver = version !== null && validateVersionFormat(version);
250
+ const rawVersion = registry.readVersion(fullPath);
251
+ const version = rawVersion !== null && validateVersionFormat(rawVersion) ? rawVersion : null;
252
+ if (rawVersion !== null && version === null) {
253
+ logger.debug('version-manager - discoverVersionFiles', 'Non-semver version skipped', {
254
+ relativePath: path.relative(repoRoot, fullPath),
255
+ rawVersion
256
+ });
257
+ }
252
258
  const descriptor = {
253
259
  path: fullPath,
254
260
  relativePath: path.relative(repoRoot, fullPath),
255
261
  type: fileType,
256
262
  projectLabel: registry.projectLabel,
257
263
  version,
258
- selected: isSemver
264
+ selected: version !== null
259
265
  };
260
266
  discoveredFiles.push(descriptor);
261
267
  logger.debug('version-manager - discoverVersionFiles', 'Found version file', {
package/package.json CHANGED
@@ -1,85 +1,84 @@
1
- {
2
- "name": "claude-git-hooks",
3
- "version": "2.66.1",
4
- "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
- "type": "module",
6
- "bin": {
7
- "claude-hooks": "./bin/claude-hooks"
8
- },
9
- "scripts": {
10
- "test": "npm run test:all",
11
- "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
- "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
- "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
- "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
- "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
- "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
- "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
- "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
- "test:e2e": "bash test/manual/sdlc-stability-check.sh",
20
- "test:full": "npm run test:all && npm run test:e2e",
21
- "lint": "eslint lib/ bin/claude-hooks .library/librarian/",
22
- "lint:fix": "eslint lib/ bin/claude-hooks .library/librarian/ --fix",
23
- "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
24
- "precommit": "npm run lint && npm run test:smoke",
25
- "prepublishOnly": "npm run test:all",
26
- "library:check": "node .library/bin/library check",
27
- "library:regenerate": "node .library/bin/library regenerate",
28
- "library:extract": "node .library/bin/library extract",
29
- "library:tokens": "node .library/bin/library tokens",
30
- "library:graph": "node .library/bin/library graph",
31
- "library:inject": "node .library/bin/library inject",
32
- "library:validate": "node .library/bin/library validate",
33
- "library:report": "node .library/bin/library report"
34
- },
35
- "keywords": [
36
- "git",
37
- "hooks",
38
- "claude",
39
- "ai",
40
- "code-review",
41
- "commit-messages",
42
- "pre-commit",
43
- "automation"
44
- ],
45
- "author": "Pablo Rovito",
46
- "license": "MIT",
47
- "repository": {
48
- "type": "git",
49
- "url": "https://github.com/mscope-S-L/git-hooks.git"
50
- },
51
- "engines": {
52
- "node": ">=16.9.0"
53
- },
54
- "engineStrict": false,
55
- "os": [
56
- "darwin",
57
- "linux",
58
- "win32"
59
- ],
60
- "preferGlobal": true,
61
- "files": [
62
- "bin/",
63
- "lib/",
64
- "templates/",
65
- "README.md",
66
- "CHANGELOG.md",
67
- "CLAUDE.md",
68
- "LICENSE"
69
- ],
70
- "dependencies": {
71
- "@anthropic-ai/sdk": "^0.91.0",
72
- "@octokit/rest": "^21.0.0",
73
- "langfuse": "^3.38.20"
74
- },
75
- "devDependencies": {
76
- "@types/jest": "^29.5.0",
77
- "eslint": "^8.57.1",
78
- "jest": "^29.7.0",
79
- "js-tiktoken": "^1.0.18",
80
- "madge": "^8.0.0",
81
- "prettier": "^3.2.0",
82
- "tree-sitter-wasms": "^0.1.13",
83
- "web-tree-sitter": "^0.24.7"
84
- }
85
- }
1
+ {
2
+ "name": "claude-git-hooks",
3
+ "version": "2.68.0",
4
+ "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-hooks": "./bin/claude-hooks"
8
+ },
9
+ "scripts": {
10
+ "test": "npm run test:all",
11
+ "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
+ "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
+ "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
+ "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
+ "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
+ "test:e2e": "bash test/manual/sdlc-stability-check.sh",
20
+ "lint": "eslint lib/ bin/claude-hooks .library/librarian/",
21
+ "lint:fix": "eslint lib/ bin/claude-hooks .library/librarian/ --fix",
22
+ "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
23
+ "precommit": "npm run lint && npm run test:smoke",
24
+ "prepublishOnly": "npm run test:all",
25
+ "library:check": "node .library/bin/library check",
26
+ "library:regenerate": "node .library/bin/library regenerate",
27
+ "library:extract": "node .library/bin/library extract",
28
+ "library:tokens": "node .library/bin/library tokens",
29
+ "library:graph": "node .library/bin/library graph",
30
+ "library:inject": "node .library/bin/library inject",
31
+ "library:validate": "node .library/bin/library validate",
32
+ "library:report": "node .library/bin/library report"
33
+ },
34
+ "keywords": [
35
+ "git",
36
+ "hooks",
37
+ "claude",
38
+ "ai",
39
+ "code-review",
40
+ "commit-messages",
41
+ "pre-commit",
42
+ "automation"
43
+ ],
44
+ "author": "Pablo Rovito",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/mscope-S-L/git-hooks.git"
49
+ },
50
+ "engines": {
51
+ "node": ">=16.9.0"
52
+ },
53
+ "engineStrict": false,
54
+ "os": [
55
+ "darwin",
56
+ "linux",
57
+ "win32"
58
+ ],
59
+ "preferGlobal": true,
60
+ "files": [
61
+ "bin/",
62
+ "lib/",
63
+ "templates/",
64
+ "README.md",
65
+ "CHANGELOG.md",
66
+ "CLAUDE.md",
67
+ "LICENSE"
68
+ ],
69
+ "dependencies": {
70
+ "@anthropic-ai/sdk": "^0.91.0",
71
+ "@octokit/rest": "^21.0.0",
72
+ "langfuse": "^3.38.20"
73
+ },
74
+ "devDependencies": {
75
+ "@types/jest": "^29.5.0",
76
+ "eslint": "^8.57.1",
77
+ "jest": "^29.7.0",
78
+ "js-tiktoken": "^1.0.18",
79
+ "madge": "^8.0.0",
80
+ "prettier": "^3.2.0",
81
+ "tree-sitter-wasms": "^0.1.13",
82
+ "web-tree-sitter": "^0.24.7"
83
+ }
84
+ }
@@ -30,7 +30,7 @@ OUTPUT_SCHEMA:
30
30
  "line":int,
31
31
  "method":"name",
32
32
  "message":"desc",
33
- "rule":"optional"
33
+ "rule":"JBE-NNN|UIK-NNN|STR-NNN|...|empty"
34
34
  }],
35
35
  "blockingIssues":[{
36
36
  "description":"text",
@@ -53,5 +53,6 @@ RULES:
53
53
  - ratings:A(0issues),B(1-2minor),C(1major|3-5minor),D(2+major|1critical),E(1+blocker|2+critical)
54
54
  - IMPORTANT: @Autowired usage in Spring is NOT a BLOCKER/CRITICAL issue (max severity: MAJOR)
55
55
  - Spring dependency injection patterns (@Autowired) should NOT block commits
56
+ - RULE CATALOGUE: if a `=== PLATFORM ANTIPATTERN CATALOGUE ===` section is present in this prompt, classify each finding against it. When a finding matches a catalogue entry, populate `details[].rule` with the rule ID exactly (e.g. "JBE-001", "UIK-002"). The catalogue's severity is the MINIMUM to assign — don't downgrade. Findings that don't match any catalogue entry leave `rule` empty (those become candidate skill-gaps for the team to review separately).
56
57
 
57
58
  ANALYZE_BELOW:
@@ -33,6 +33,12 @@
33
33
  "model": "opus"
34
34
  },
35
35
 
36
+ "skillRegistry": {
37
+ "enabled": true,
38
+ "blockOn": "never",
39
+ "resumeOnCreatePr": true
40
+ },
41
+
36
42
  "prAnalysis": {
37
43
  "model": "sonnet",
38
44
  "timeout": 300000,
@@ -55,6 +61,11 @@
55
61
  "failOnError": true,
56
62
  "failOnWarning": false,
57
63
  "timeout": 30000
64
+ },
65
+
66
+ "autoUpdate": {
67
+ "enabled": true,
68
+ "intervalHours": 24
58
69
  }
59
70
  },
60
71
 
@@ -90,6 +101,25 @@
90
101
  "use_case": "Override the default sonnet model for judge passes"
91
102
  },
92
103
 
104
+ "skillRegistry.enabled": {
105
+ "description": "Enable/disable the mscope automation-skills integration: deterministic rule checks in pre-commit, antipattern catalogue injection into the analysis prompt, and the skill-gap writer. NOTE the cross-repo side effect: when the sibling automation-skills repo is found, unclassified AI findings are appended as [skill-gap] candidates to that repo's skill-feedback.md (reviewed via `automation-skills retro`; never auto-merged into the registry).",
106
+ "default": "true",
107
+ "use_case": "Set to false to fully disable the skill-registry integration (it auto-skips anyway when the skill repo isn't found)"
108
+ },
109
+
110
+ "skillRegistry.blockOn": {
111
+ "description": "Severity threshold at which deterministic skill-registry findings block the commit",
112
+ "default": "never",
113
+ "examples": ["never", "critical", "high", "medium", "low"],
114
+ "use_case": "Set to 'critical' or 'high' once the team is ready to enforce the mscope rule catalogue"
115
+ },
116
+
117
+ "skillRegistry.resumeOnCreatePr": {
118
+ "description": "Run `automation-skills resume` (interactive lessons-learned capture to the skill repo's implementation-history.md) during create-pr, before tags are pushed. Skipped automatically in headless mode or when the automation-skills CLI is not installed.",
119
+ "default": "true",
120
+ "use_case": "Set to false if your team captures lessons through another channel"
121
+ },
122
+
93
123
  "prAnalysis.model": {
94
124
  "description": "Claude model for PR analysis",
95
125
  "default": "sonnet",
@@ -143,6 +173,18 @@
143
173
  "description": "Timeout in milliseconds for each linter execution",
144
174
  "default": "30000",
145
175
  "use_case": "Increase for large projects where linters take longer"
176
+ },
177
+
178
+ "autoUpdate.enabled": {
179
+ "description": "Enable the silent, throttled pre-command auto-update check. When a newer published version is detected before running a command, claude-git-hooks updates itself globally, reinstalls hooks (--force), and asks you to re-run your command. Excludes update/install/uninstall/help/version/migrate-config, and is skipped in --headless mode. The manual `claude-hooks update` command runs verbosely regardless of this setting.",
180
+ "default": "true",
181
+ "use_case": "Set to false to disable automatic background updates and rely only on `claude-hooks update`"
182
+ },
183
+
184
+ "autoUpdate.intervalHours": {
185
+ "description": "Minimum hours between pre-command auto-update checks. Throttle state is stored in .claude/.last-update-check (gitignored).",
186
+ "default": "24",
187
+ "use_case": "Lower it for faster propagation of releases, or raise it to reduce network checks"
146
188
  }
147
189
  },
148
190