@hasna/hooks 0.0.7 → 0.1.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.
Files changed (48) hide show
  1. package/bin/index.js +157 -2
  2. package/dist/index.js +156 -1
  3. package/hooks/hook-autoformat/README.md +39 -0
  4. package/hooks/hook-autoformat/package.json +58 -0
  5. package/hooks/hook-autoformat/src/hook.ts +223 -0
  6. package/hooks/hook-autostage/README.md +70 -0
  7. package/hooks/hook-autostage/package.json +12 -0
  8. package/hooks/hook-autostage/src/hook.ts +167 -0
  9. package/hooks/hook-commandlog/README.md +45 -0
  10. package/hooks/hook-commandlog/package.json +12 -0
  11. package/hooks/hook-commandlog/src/hook.ts +92 -0
  12. package/hooks/hook-costwatch/README.md +61 -0
  13. package/hooks/hook-costwatch/package.json +12 -0
  14. package/hooks/hook-costwatch/src/hook.ts +178 -0
  15. package/hooks/hook-desktopnotify/README.md +50 -0
  16. package/hooks/hook-desktopnotify/package.json +57 -0
  17. package/hooks/hook-desktopnotify/src/hook.ts +112 -0
  18. package/hooks/hook-envsetup/README.md +40 -0
  19. package/hooks/hook-envsetup/package.json +58 -0
  20. package/hooks/hook-envsetup/src/hook.ts +197 -0
  21. package/hooks/hook-errornotify/README.md +66 -0
  22. package/hooks/hook-errornotify/package.json +12 -0
  23. package/hooks/hook-errornotify/src/hook.ts +197 -0
  24. package/hooks/hook-permissionguard/README.md +48 -0
  25. package/hooks/hook-permissionguard/package.json +58 -0
  26. package/hooks/hook-permissionguard/src/hook.ts +268 -0
  27. package/hooks/hook-promptguard/README.md +64 -0
  28. package/hooks/hook-promptguard/package.json +12 -0
  29. package/hooks/hook-promptguard/src/hook.ts +200 -0
  30. package/hooks/hook-protectfiles/README.md +62 -0
  31. package/hooks/hook-protectfiles/package.json +58 -0
  32. package/hooks/hook-protectfiles/src/hook.ts +267 -0
  33. package/hooks/hook-sessionlog/README.md +48 -0
  34. package/hooks/hook-sessionlog/package.json +12 -0
  35. package/hooks/hook-sessionlog/src/hook.ts +100 -0
  36. package/hooks/hook-slacknotify/README.md +62 -0
  37. package/hooks/hook-slacknotify/package.json +12 -0
  38. package/hooks/hook-slacknotify/src/hook.ts +146 -0
  39. package/hooks/hook-soundnotify/README.md +63 -0
  40. package/hooks/hook-soundnotify/package.json +12 -0
  41. package/hooks/hook-soundnotify/src/hook.ts +173 -0
  42. package/hooks/hook-taskgate/README.md +62 -0
  43. package/hooks/hook-taskgate/package.json +12 -0
  44. package/hooks/hook-taskgate/src/hook.ts +169 -0
  45. package/hooks/hook-tddguard/README.md +50 -0
  46. package/hooks/hook-tddguard/package.json +12 -0
  47. package/hooks/hook-tddguard/src/hook.ts +263 -0
  48. package/package.json +2 -2
package/bin/index.js CHANGED
@@ -1886,7 +1886,12 @@ var init_registry = __esm(() => {
1886
1886
  "Code Quality",
1887
1887
  "Security",
1888
1888
  "Notifications",
1889
- "Context Management"
1889
+ "Context Management",
1890
+ "Workflow Automation",
1891
+ "Environment",
1892
+ "Permissions",
1893
+ "Observability",
1894
+ "Agent Teams"
1890
1895
  ];
1891
1896
  HOOKS = [
1892
1897
  {
@@ -2038,6 +2043,156 @@ var init_registry = __esm(() => {
2038
2043
  event: "Notification",
2039
2044
  matcher: "",
2040
2045
  tags: ["context", "compaction", "state", "backup"]
2046
+ },
2047
+ {
2048
+ name: "autoformat",
2049
+ displayName: "Auto Format",
2050
+ description: "Runs project formatter (Prettier, Biome, Ruff, Black, gofmt) after file edits",
2051
+ version: "0.1.0",
2052
+ category: "Workflow Automation",
2053
+ event: "PostToolUse",
2054
+ matcher: "Edit|Write",
2055
+ tags: ["format", "prettier", "biome", "ruff", "black", "gofmt", "style"]
2056
+ },
2057
+ {
2058
+ name: "autostage",
2059
+ displayName: "Auto Stage",
2060
+ description: "Automatically git-stages files after Claude edits them",
2061
+ version: "0.1.0",
2062
+ category: "Workflow Automation",
2063
+ event: "PostToolUse",
2064
+ matcher: "Edit|Write",
2065
+ tags: ["git", "stage", "add", "auto"]
2066
+ },
2067
+ {
2068
+ name: "tddguard",
2069
+ displayName: "TDD Guard",
2070
+ description: "Blocks implementation edits unless corresponding test files exist",
2071
+ version: "0.1.0",
2072
+ category: "Workflow Automation",
2073
+ event: "PreToolUse",
2074
+ matcher: "Edit|Write",
2075
+ tags: ["tdd", "tests", "red-green-refactor", "enforcement"]
2076
+ },
2077
+ {
2078
+ name: "envsetup",
2079
+ displayName: "Env Setup",
2080
+ description: "Warns when nvm, virtualenv, asdf, or rbenv may need activation before commands",
2081
+ version: "0.1.0",
2082
+ category: "Environment",
2083
+ event: "PreToolUse",
2084
+ matcher: "Bash",
2085
+ tags: ["nvm", "virtualenv", "asdf", "rbenv", "environment", "python", "node"]
2086
+ },
2087
+ {
2088
+ name: "permissionguard",
2089
+ displayName: "Permission Guard",
2090
+ description: "Auto-approves safe read-only commands and blocks dangerous operations",
2091
+ version: "0.1.0",
2092
+ category: "Permissions",
2093
+ event: "PreToolUse",
2094
+ matcher: "Bash",
2095
+ tags: ["permission", "allowlist", "blocklist", "safety", "auto-approve"]
2096
+ },
2097
+ {
2098
+ name: "protectfiles",
2099
+ displayName: "Protect Files",
2100
+ description: "Blocks access to .env, secrets, SSH keys, and lock files",
2101
+ version: "0.1.0",
2102
+ category: "Permissions",
2103
+ event: "PreToolUse",
2104
+ matcher: "Edit|Write|Read|Bash",
2105
+ tags: ["security", "env", "secrets", "keys", "lock-files", "protect"]
2106
+ },
2107
+ {
2108
+ name: "promptguard",
2109
+ displayName: "Prompt Guard",
2110
+ description: "Blocks prompt injection attempts and credential access requests",
2111
+ version: "0.1.0",
2112
+ category: "Permissions",
2113
+ event: "PreToolUse",
2114
+ matcher: "",
2115
+ tags: ["prompt", "injection", "security", "validation", "guard"]
2116
+ },
2117
+ {
2118
+ name: "desktopnotify",
2119
+ displayName: "Desktop Notify",
2120
+ description: "Sends native desktop notifications via osascript (macOS) or notify-send (Linux)",
2121
+ version: "0.1.0",
2122
+ category: "Notifications",
2123
+ event: "Stop",
2124
+ matcher: "",
2125
+ tags: ["notification", "desktop", "macos", "linux", "native"]
2126
+ },
2127
+ {
2128
+ name: "slacknotify",
2129
+ displayName: "Slack Notify",
2130
+ description: "Sends Slack webhook notifications when Claude finishes",
2131
+ version: "0.1.0",
2132
+ category: "Notifications",
2133
+ event: "Stop",
2134
+ matcher: "",
2135
+ tags: ["notification", "slack", "webhook", "team"]
2136
+ },
2137
+ {
2138
+ name: "soundnotify",
2139
+ displayName: "Sound Notify",
2140
+ description: "Plays a system sound when Claude finishes (macOS/Linux)",
2141
+ version: "0.1.0",
2142
+ category: "Notifications",
2143
+ event: "Stop",
2144
+ matcher: "",
2145
+ tags: ["notification", "sound", "audio", "alert"]
2146
+ },
2147
+ {
2148
+ name: "sessionlog",
2149
+ displayName: "Session Log",
2150
+ description: "Logs every tool call to .claude/session-log-<date>.jsonl",
2151
+ version: "0.1.0",
2152
+ category: "Observability",
2153
+ event: "PostToolUse",
2154
+ matcher: "",
2155
+ tags: ["logging", "audit", "session", "history", "jsonl"]
2156
+ },
2157
+ {
2158
+ name: "commandlog",
2159
+ displayName: "Command Log",
2160
+ description: "Logs every bash command Claude runs to .claude/commands.log",
2161
+ version: "0.1.0",
2162
+ category: "Observability",
2163
+ event: "PostToolUse",
2164
+ matcher: "Bash",
2165
+ tags: ["logging", "bash", "commands", "audit"]
2166
+ },
2167
+ {
2168
+ name: "costwatch",
2169
+ displayName: "Cost Watch",
2170
+ description: "Estimates session token usage and warns when budget threshold is exceeded",
2171
+ version: "0.1.0",
2172
+ category: "Observability",
2173
+ event: "Stop",
2174
+ matcher: "",
2175
+ tags: ["cost", "tokens", "budget", "usage", "monitoring"]
2176
+ },
2177
+ {
2178
+ name: "errornotify",
2179
+ displayName: "Error Notify",
2180
+ description: "Detects tool failures and logs errors to .claude/errors.log",
2181
+ version: "0.1.0",
2182
+ category: "Observability",
2183
+ event: "PostToolUse",
2184
+ matcher: "",
2185
+ tags: ["errors", "failures", "logging", "debugging"]
2186
+ },
2187
+ {
2188
+ name: "taskgate",
2189
+ displayName: "Task Gate",
2190
+ description: "Validates task completion criteria before allowing tasks to be marked done",
2191
+ version: "0.1.0",
2192
+ category: "Agent Teams",
2193
+ event: "PostToolUse",
2194
+ matcher: "",
2195
+ tags: ["tasks", "completion", "gate", "quality", "agent-teams"]
2041
2196
  }
2042
2197
  ];
2043
2198
  });
@@ -5270,7 +5425,7 @@ function resolveScope(options) {
5270
5425
  return "project";
5271
5426
  return "global";
5272
5427
  }
5273
- program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.7");
5428
+ program2.name("hooks").description("Install Claude Code hooks for your project").version("0.1.0");
5274
5429
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
5275
5430
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
5276
5431
  });
package/dist/index.js CHANGED
@@ -5,7 +5,12 @@ var CATEGORIES = [
5
5
  "Code Quality",
6
6
  "Security",
7
7
  "Notifications",
8
- "Context Management"
8
+ "Context Management",
9
+ "Workflow Automation",
10
+ "Environment",
11
+ "Permissions",
12
+ "Observability",
13
+ "Agent Teams"
9
14
  ];
10
15
  var HOOKS = [
11
16
  {
@@ -157,6 +162,156 @@ var HOOKS = [
157
162
  event: "Notification",
158
163
  matcher: "",
159
164
  tags: ["context", "compaction", "state", "backup"]
165
+ },
166
+ {
167
+ name: "autoformat",
168
+ displayName: "Auto Format",
169
+ description: "Runs project formatter (Prettier, Biome, Ruff, Black, gofmt) after file edits",
170
+ version: "0.1.0",
171
+ category: "Workflow Automation",
172
+ event: "PostToolUse",
173
+ matcher: "Edit|Write",
174
+ tags: ["format", "prettier", "biome", "ruff", "black", "gofmt", "style"]
175
+ },
176
+ {
177
+ name: "autostage",
178
+ displayName: "Auto Stage",
179
+ description: "Automatically git-stages files after Claude edits them",
180
+ version: "0.1.0",
181
+ category: "Workflow Automation",
182
+ event: "PostToolUse",
183
+ matcher: "Edit|Write",
184
+ tags: ["git", "stage", "add", "auto"]
185
+ },
186
+ {
187
+ name: "tddguard",
188
+ displayName: "TDD Guard",
189
+ description: "Blocks implementation edits unless corresponding test files exist",
190
+ version: "0.1.0",
191
+ category: "Workflow Automation",
192
+ event: "PreToolUse",
193
+ matcher: "Edit|Write",
194
+ tags: ["tdd", "tests", "red-green-refactor", "enforcement"]
195
+ },
196
+ {
197
+ name: "envsetup",
198
+ displayName: "Env Setup",
199
+ description: "Warns when nvm, virtualenv, asdf, or rbenv may need activation before commands",
200
+ version: "0.1.0",
201
+ category: "Environment",
202
+ event: "PreToolUse",
203
+ matcher: "Bash",
204
+ tags: ["nvm", "virtualenv", "asdf", "rbenv", "environment", "python", "node"]
205
+ },
206
+ {
207
+ name: "permissionguard",
208
+ displayName: "Permission Guard",
209
+ description: "Auto-approves safe read-only commands and blocks dangerous operations",
210
+ version: "0.1.0",
211
+ category: "Permissions",
212
+ event: "PreToolUse",
213
+ matcher: "Bash",
214
+ tags: ["permission", "allowlist", "blocklist", "safety", "auto-approve"]
215
+ },
216
+ {
217
+ name: "protectfiles",
218
+ displayName: "Protect Files",
219
+ description: "Blocks access to .env, secrets, SSH keys, and lock files",
220
+ version: "0.1.0",
221
+ category: "Permissions",
222
+ event: "PreToolUse",
223
+ matcher: "Edit|Write|Read|Bash",
224
+ tags: ["security", "env", "secrets", "keys", "lock-files", "protect"]
225
+ },
226
+ {
227
+ name: "promptguard",
228
+ displayName: "Prompt Guard",
229
+ description: "Blocks prompt injection attempts and credential access requests",
230
+ version: "0.1.0",
231
+ category: "Permissions",
232
+ event: "PreToolUse",
233
+ matcher: "",
234
+ tags: ["prompt", "injection", "security", "validation", "guard"]
235
+ },
236
+ {
237
+ name: "desktopnotify",
238
+ displayName: "Desktop Notify",
239
+ description: "Sends native desktop notifications via osascript (macOS) or notify-send (Linux)",
240
+ version: "0.1.0",
241
+ category: "Notifications",
242
+ event: "Stop",
243
+ matcher: "",
244
+ tags: ["notification", "desktop", "macos", "linux", "native"]
245
+ },
246
+ {
247
+ name: "slacknotify",
248
+ displayName: "Slack Notify",
249
+ description: "Sends Slack webhook notifications when Claude finishes",
250
+ version: "0.1.0",
251
+ category: "Notifications",
252
+ event: "Stop",
253
+ matcher: "",
254
+ tags: ["notification", "slack", "webhook", "team"]
255
+ },
256
+ {
257
+ name: "soundnotify",
258
+ displayName: "Sound Notify",
259
+ description: "Plays a system sound when Claude finishes (macOS/Linux)",
260
+ version: "0.1.0",
261
+ category: "Notifications",
262
+ event: "Stop",
263
+ matcher: "",
264
+ tags: ["notification", "sound", "audio", "alert"]
265
+ },
266
+ {
267
+ name: "sessionlog",
268
+ displayName: "Session Log",
269
+ description: "Logs every tool call to .claude/session-log-<date>.jsonl",
270
+ version: "0.1.0",
271
+ category: "Observability",
272
+ event: "PostToolUse",
273
+ matcher: "",
274
+ tags: ["logging", "audit", "session", "history", "jsonl"]
275
+ },
276
+ {
277
+ name: "commandlog",
278
+ displayName: "Command Log",
279
+ description: "Logs every bash command Claude runs to .claude/commands.log",
280
+ version: "0.1.0",
281
+ category: "Observability",
282
+ event: "PostToolUse",
283
+ matcher: "Bash",
284
+ tags: ["logging", "bash", "commands", "audit"]
285
+ },
286
+ {
287
+ name: "costwatch",
288
+ displayName: "Cost Watch",
289
+ description: "Estimates session token usage and warns when budget threshold is exceeded",
290
+ version: "0.1.0",
291
+ category: "Observability",
292
+ event: "Stop",
293
+ matcher: "",
294
+ tags: ["cost", "tokens", "budget", "usage", "monitoring"]
295
+ },
296
+ {
297
+ name: "errornotify",
298
+ displayName: "Error Notify",
299
+ description: "Detects tool failures and logs errors to .claude/errors.log",
300
+ version: "0.1.0",
301
+ category: "Observability",
302
+ event: "PostToolUse",
303
+ matcher: "",
304
+ tags: ["errors", "failures", "logging", "debugging"]
305
+ },
306
+ {
307
+ name: "taskgate",
308
+ displayName: "Task Gate",
309
+ description: "Validates task completion criteria before allowing tasks to be marked done",
310
+ version: "0.1.0",
311
+ category: "Agent Teams",
312
+ event: "PostToolUse",
313
+ matcher: "",
314
+ tags: ["tasks", "completion", "gate", "quality", "agent-teams"]
160
315
  }
161
316
  ];
162
317
  function getHooksByCategory(category) {
@@ -0,0 +1,39 @@
1
+ # hook-autoformat
2
+
3
+ Claude Code hook that automatically runs the project's formatter after file edits.
4
+
5
+ ## Overview
6
+
7
+ Detects and runs the appropriate formatter whenever Claude edits or writes a file. No configuration needed — it reads your project's existing formatter config.
8
+
9
+ ## Supported Formatters
10
+
11
+ | Config File | Formatter | File Types |
12
+ |-------------|-----------|------------|
13
+ | `.prettierrc` / `prettier` in package.json | Prettier | JS, TS, CSS, HTML, MD, JSON, YAML, etc. |
14
+ | `biome.json` | Biome | JS, TS, JSON, CSS, GraphQL |
15
+ | `pyproject.toml` with `[tool.ruff]` | Ruff | Python |
16
+ | `pyproject.toml` with `[tool.black]` | Black | Python |
17
+ | `.clang-format` | clang-format | C, C++, Obj-C |
18
+ | (any `.go` file) | gofmt | Go |
19
+
20
+ ## Hook Event
21
+
22
+ - **PostToolUse** (matcher: `Edit|Write`)
23
+
24
+ ## Behavior
25
+
26
+ 1. Fires after every `Edit` or `Write` tool call
27
+ 2. Reads `tool_input.file_path` to get the edited file
28
+ 3. Detects the project formatter from config files in the working directory
29
+ 4. Runs the formatter as a subprocess
30
+ 5. Logs the result to stderr
31
+ 6. Always outputs `{ continue: true }`
32
+
33
+ ## Priority
34
+
35
+ If both Biome and Prettier configs exist, Biome takes priority (it's faster).
36
+
37
+ ## License
38
+
39
+ MIT
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@hasna/hook-autoformat",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that auto-runs formatters after file edits",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-autoformat": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/hook.js",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/hook.js",
13
+ "types": "./dist/hook.d.ts"
14
+ },
15
+ "./cli": {
16
+ "import": "./dist/cli.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "bun build ./src/hook.ts --outdir ./dist --target node",
25
+ "prepublishOnly": "bun run build",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "keywords": [
29
+ "claude-code",
30
+ "claude",
31
+ "hook",
32
+ "formatter",
33
+ "autoformat",
34
+ "prettier",
35
+ "biome",
36
+ "ruff",
37
+ "cli"
38
+ ],
39
+ "author": "Hasna",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/hasna/open-hooks.git"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org/"
48
+ },
49
+ "engines": {
50
+ "node": ">=18",
51
+ "bun": ">=1.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/bun": "^1.3.8",
55
+ "@types/node": "^20",
56
+ "typescript": "^5.0.0"
57
+ }
58
+ }
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: autoformat
5
+ *
6
+ * PostToolUse hook that auto-runs the project's formatter after file edits.
7
+ * Detects the formatter from project config files:
8
+ *
9
+ * - .prettierrc / prettier in package.json → bunx prettier --write <file>
10
+ * - biome.json → bunx biome format --write <file>
11
+ * - pyproject.toml with [tool.ruff] or [tool.black] → ruff format / black
12
+ * - .clang-format → clang-format -i <file>
13
+ * - Go files (.go) → gofmt -w <file>
14
+ */
15
+
16
+ import { readFileSync, existsSync } from "fs";
17
+ import { join, extname } from "path";
18
+ import { execSync } from "child_process";
19
+
20
+ interface HookInput {
21
+ session_id: string;
22
+ cwd: string;
23
+ tool_name: string;
24
+ tool_input: Record<string, unknown>;
25
+ tool_output?: string;
26
+ }
27
+
28
+ interface HookOutput {
29
+ continue: boolean;
30
+ }
31
+
32
+ function readStdinJson(): HookInput | null {
33
+ try {
34
+ const input = readFileSync(0, "utf-8").trim();
35
+ if (!input) return null;
36
+ return JSON.parse(input);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function respond(output: HookOutput): void {
43
+ console.log(JSON.stringify(output));
44
+ }
45
+
46
+ function hasPrettierConfig(cwd: string): boolean {
47
+ const configFiles = [
48
+ ".prettierrc",
49
+ ".prettierrc.json",
50
+ ".prettierrc.yml",
51
+ ".prettierrc.yaml",
52
+ ".prettierrc.js",
53
+ ".prettierrc.cjs",
54
+ ".prettierrc.mjs",
55
+ "prettier.config.js",
56
+ "prettier.config.cjs",
57
+ "prettier.config.mjs",
58
+ ];
59
+
60
+ for (const file of configFiles) {
61
+ if (existsSync(join(cwd, file))) return true;
62
+ }
63
+
64
+ // Check package.json for prettier key
65
+ const packageJsonPath = join(cwd, "package.json");
66
+ if (existsSync(packageJsonPath)) {
67
+ try {
68
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
69
+ if (pkg.prettier) return true;
70
+ } catch {
71
+ // ignore
72
+ }
73
+ }
74
+
75
+ return false;
76
+ }
77
+
78
+ function hasBiomeConfig(cwd: string): boolean {
79
+ return existsSync(join(cwd, "biome.json")) || existsSync(join(cwd, "biome.jsonc"));
80
+ }
81
+
82
+ function getPythonFormatter(cwd: string): "ruff" | "black" | null {
83
+ const pyprojectPath = join(cwd, "pyproject.toml");
84
+ if (!existsSync(pyprojectPath)) return null;
85
+
86
+ try {
87
+ const content = readFileSync(pyprojectPath, "utf-8");
88
+ if (content.includes("[tool.ruff]")) return "ruff";
89
+ if (content.includes("[tool.black]")) return "black";
90
+ } catch {
91
+ // ignore
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ function hasClangFormat(cwd: string): boolean {
98
+ return existsSync(join(cwd, ".clang-format"));
99
+ }
100
+
101
+ function isGoFile(filePath: string): boolean {
102
+ return extname(filePath) === ".go";
103
+ }
104
+
105
+ function isPythonFile(filePath: string): boolean {
106
+ return extname(filePath) === ".py";
107
+ }
108
+
109
+ function isFormattableByPrettier(filePath: string): boolean {
110
+ const ext = extname(filePath).toLowerCase();
111
+ const prettierExts = [
112
+ ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
113
+ ".json", ".css", ".scss", ".less", ".html",
114
+ ".md", ".mdx", ".yaml", ".yml", ".graphql",
115
+ ".vue", ".svelte",
116
+ ];
117
+ return prettierExts.includes(ext);
118
+ }
119
+
120
+ function isFormattableByBiome(filePath: string): boolean {
121
+ const ext = extname(filePath).toLowerCase();
122
+ const biomeExts = [
123
+ ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
124
+ ".json", ".jsonc", ".css", ".graphql",
125
+ ];
126
+ return biomeExts.includes(ext);
127
+ }
128
+
129
+ function isClangFormattable(filePath: string): boolean {
130
+ const ext = extname(filePath).toLowerCase();
131
+ const clangExts = [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hxx", ".m", ".mm"];
132
+ return clangExts.includes(ext);
133
+ }
134
+
135
+ function detectFormatter(cwd: string, filePath: string): { name: string; command: string } | null {
136
+ // Go files always use gofmt
137
+ if (isGoFile(filePath)) {
138
+ return { name: "gofmt", command: `gofmt -w "${filePath}"` };
139
+ }
140
+
141
+ // Python files
142
+ if (isPythonFile(filePath)) {
143
+ const pyFormatter = getPythonFormatter(cwd);
144
+ if (pyFormatter === "ruff") {
145
+ return { name: "ruff", command: `ruff format "${filePath}"` };
146
+ }
147
+ if (pyFormatter === "black") {
148
+ return { name: "black", command: `black "${filePath}"` };
149
+ }
150
+ return null;
151
+ }
152
+
153
+ // C/C++ files with .clang-format
154
+ if (isClangFormattable(filePath) && hasClangFormat(cwd)) {
155
+ return { name: "clang-format", command: `clang-format -i "${filePath}"` };
156
+ }
157
+
158
+ // Biome takes priority over Prettier if both exist (it's faster)
159
+ if (hasBiomeConfig(cwd) && isFormattableByBiome(filePath)) {
160
+ return { name: "biome", command: `bunx @biomejs/biome format --write "${filePath}"` };
161
+ }
162
+
163
+ // Prettier
164
+ if (hasPrettierConfig(cwd) && isFormattableByPrettier(filePath)) {
165
+ return { name: "prettier", command: `bunx prettier --write "${filePath}"` };
166
+ }
167
+
168
+ return null;
169
+ }
170
+
171
+ function runFormatter(cwd: string, name: string, command: string): void {
172
+ try {
173
+ execSync(command, {
174
+ cwd,
175
+ encoding: "utf-8",
176
+ stdio: ["pipe", "pipe", "pipe"],
177
+ timeout: 30000, // 30s timeout
178
+ });
179
+ console.error(`[hook-autoformat] Formatted with ${name}`);
180
+ } catch (error: unknown) {
181
+ const execError = error as { stderr?: string; message?: string };
182
+ const errorMsg = execError.stderr || execError.message || "unknown error";
183
+ console.error(`[hook-autoformat] ${name} failed: ${errorMsg}`);
184
+ }
185
+ }
186
+
187
+ export function run(): void {
188
+ const input = readStdinJson();
189
+
190
+ if (!input) {
191
+ respond({ continue: true });
192
+ return;
193
+ }
194
+
195
+ // Only process Edit and Write tools
196
+ if (input.tool_name !== "Edit" && input.tool_name !== "Write") {
197
+ respond({ continue: true });
198
+ return;
199
+ }
200
+
201
+ const filePath = input.tool_input?.file_path as string;
202
+ if (!filePath || typeof filePath !== "string") {
203
+ respond({ continue: true });
204
+ return;
205
+ }
206
+
207
+ const cwd = input.cwd || process.cwd();
208
+ const formatter = detectFormatter(cwd, filePath);
209
+
210
+ if (!formatter) {
211
+ respond({ continue: true });
212
+ return;
213
+ }
214
+
215
+ console.error(`[hook-autoformat] Running ${formatter.name} on ${filePath}`);
216
+ runFormatter(cwd, formatter.name, formatter.command);
217
+
218
+ respond({ continue: true });
219
+ }
220
+
221
+ if (import.meta.main) {
222
+ run();
223
+ }