@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
@@ -0,0 +1,70 @@
1
+ # hook-autostage
2
+
3
+ Claude Code hook that automatically stages files after Claude edits or writes them.
4
+
5
+ ## Event
6
+
7
+ **PostToolUse** (matcher: `Edit|Write`)
8
+
9
+ ## What It Does
10
+
11
+ After Claude modifies a file via `Edit` or `Write`, this hook automatically runs `git add <file>` to stage the change. This keeps your git staging area in sync with Claude's edits without manual intervention.
12
+
13
+ ### Safety Checks
14
+
15
+ Before staging, the hook verifies:
16
+
17
+ 1. **Git repo exists** — checks that `cwd` is inside a git repository
18
+ 2. **File exists** — confirms the file was actually created/modified
19
+ 3. **Not gitignored** — runs `git check-ignore` to skip ignored files
20
+
21
+ ### What Gets Staged
22
+
23
+ - Files modified by Claude's `Edit` tool
24
+ - Files created/written by Claude's `Write` tool
25
+
26
+ ### What Does NOT Get Staged
27
+
28
+ - Files in `.gitignore` (e.g., `node_modules/`, `.env`, `dist/`)
29
+ - Files outside git repositories
30
+ - Files from other tools (Bash, Read, etc.)
31
+
32
+ ## Installation
33
+
34
+ Add to your `.claude/settings.json`:
35
+
36
+ ```json
37
+ {
38
+ "hooks": {
39
+ "PostToolUse": [
40
+ {
41
+ "matcher": "Edit|Write",
42
+ "hooks": [
43
+ {
44
+ "type": "command",
45
+ "command": "bun run hooks/hook-autostage/src/hook.ts"
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Output
55
+
56
+ Always `{ "continue": true }` — this hook never blocks.
57
+
58
+ ## Logs
59
+
60
+ Activity is logged to stderr:
61
+
62
+ ```
63
+ [hook-autostage] Staged: src/index.ts
64
+ [hook-autostage] File is gitignored, skipping: dist/bundle.js
65
+ [hook-autostage] Not a git repo, skipping
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hook-autostage",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that auto-stages files after Claude edits them",
5
+ "type": "module",
6
+ "main": "./src/hook.ts",
7
+ "scripts": {
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "author": "Hasna",
11
+ "license": "MIT"
12
+ }
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: autostage
5
+ *
6
+ * PostToolUse hook that automatically runs `git add <file>` after
7
+ * Claude edits or writes a file. Only stages if:
8
+ * - The project is a git repo (.git directory exists)
9
+ * - The file is not in .gitignore
10
+ *
11
+ * Matcher: Edit|Write
12
+ * Always outputs { continue: true } — never blocks.
13
+ */
14
+
15
+ import { readFileSync, existsSync } from "fs";
16
+ import { execSync } from "child_process";
17
+ import { join, resolve } from "path";
18
+
19
+ interface HookInput {
20
+ session_id: string;
21
+ cwd: string;
22
+ tool_name: string;
23
+ tool_input: Record<string, unknown>;
24
+ }
25
+
26
+ interface HookOutput {
27
+ continue: true;
28
+ }
29
+
30
+ /**
31
+ * Read and parse JSON from stdin
32
+ */
33
+ function readStdinJson(): HookInput | null {
34
+ try {
35
+ const input = readFileSync(0, "utf-8").trim();
36
+ if (!input) return null;
37
+ return JSON.parse(input);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Check if a directory is inside a git repository
45
+ */
46
+ function isGitRepo(cwd: string): boolean {
47
+ try {
48
+ execSync("git rev-parse --is-inside-work-tree", {
49
+ cwd,
50
+ stdio: "pipe",
51
+ });
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check if a file is ignored by .gitignore
60
+ */
61
+ function isGitIgnored(cwd: string, filePath: string): boolean {
62
+ try {
63
+ // git check-ignore exits 0 if file IS ignored, 1 if NOT ignored
64
+ execSync(`git check-ignore -q "${filePath}"`, {
65
+ cwd,
66
+ stdio: "pipe",
67
+ });
68
+ return true; // Exit 0 → file is ignored
69
+ } catch {
70
+ return false; // Exit 1 → file is NOT ignored
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Stage a file with git add
76
+ */
77
+ function stageFile(cwd: string, filePath: string): boolean {
78
+ try {
79
+ execSync(`git add "${filePath}"`, {
80
+ cwd,
81
+ stdio: "pipe",
82
+ });
83
+ return true;
84
+ } catch (error) {
85
+ const errMsg = error instanceof Error ? error.message : String(error);
86
+ console.error(`[hook-autostage] Failed to stage ${filePath}: ${errMsg}`);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Extract file path from tool input
93
+ */
94
+ function getFilePath(toolInput: Record<string, unknown>): string | null {
95
+ return (toolInput.file_path as string) || null;
96
+ }
97
+
98
+ /**
99
+ * Output hook response
100
+ */
101
+ function respond(): void {
102
+ const output: HookOutput = { continue: true };
103
+ console.log(JSON.stringify(output));
104
+ }
105
+
106
+ /**
107
+ * Main hook execution
108
+ */
109
+ export function run(): void {
110
+ const input = readStdinJson();
111
+
112
+ if (!input) {
113
+ respond();
114
+ return;
115
+ }
116
+
117
+ // Only handle Edit and Write tools
118
+ if (input.tool_name !== "Edit" && input.tool_name !== "Write") {
119
+ respond();
120
+ return;
121
+ }
122
+
123
+ const filePath = getFilePath(input.tool_input || {});
124
+
125
+ if (!filePath) {
126
+ console.error("[hook-autostage] No file_path found in tool_input");
127
+ respond();
128
+ return;
129
+ }
130
+
131
+ const cwd = input.cwd;
132
+
133
+ // Check if this is a git repo
134
+ if (!isGitRepo(cwd)) {
135
+ console.error("[hook-autostage] Not a git repo, skipping");
136
+ respond();
137
+ return;
138
+ }
139
+
140
+ // Resolve the file path relative to cwd
141
+ const absPath = resolve(cwd, filePath);
142
+
143
+ // Check if file exists
144
+ if (!existsSync(absPath)) {
145
+ console.error(`[hook-autostage] File does not exist: ${filePath}`);
146
+ respond();
147
+ return;
148
+ }
149
+
150
+ // Check if file is gitignored
151
+ if (isGitIgnored(cwd, filePath)) {
152
+ console.error(`[hook-autostage] File is gitignored, skipping: ${filePath}`);
153
+ respond();
154
+ return;
155
+ }
156
+
157
+ // Stage the file
158
+ if (stageFile(cwd, filePath)) {
159
+ console.error(`[hook-autostage] Staged: ${filePath}`);
160
+ }
161
+
162
+ respond();
163
+ }
164
+
165
+ if (import.meta.main) {
166
+ run();
167
+ }
@@ -0,0 +1,45 @@
1
+ # hook-commandlog
2
+
3
+ Claude Code hook that logs every bash command Claude runs to a log file.
4
+
5
+ ## Overview
6
+
7
+ Every time Claude executes a Bash command, this hook appends it to `.claude/commands.log` in the project directory. Provides a clear audit trail of all shell commands run during a session.
8
+
9
+ ## Event
10
+
11
+ - **PostToolUse** (matcher: `Bash`)
12
+
13
+ ## Log Format
14
+
15
+ Each line in the log file:
16
+
17
+ ```
18
+ [2026-02-14T10:30:00.000Z] exit=0 npm install express
19
+ [2026-02-14T10:30:05.000Z] exit=0 git status
20
+ [2026-02-14T10:30:10.000Z] ls -la src/
21
+ ```
22
+
23
+ - ISO 8601 timestamp in brackets
24
+ - Exit code (if available in tool input)
25
+ - The full command string
26
+
27
+ ## Behavior
28
+
29
+ - Only logs `Bash` tool calls (other tools are ignored)
30
+ - Creates `.claude/` directory if it does not exist
31
+ - Appends to the log file (never overwrites)
32
+ - Non-blocking: logging failures are logged to stderr but never interrupt Claude
33
+ - Outputs `{ "continue": true }` always
34
+
35
+ ## Log Location
36
+
37
+ ```
38
+ <project-root>/
39
+ └── .claude/
40
+ └── commands.log
41
+ ```
42
+
43
+ ## License
44
+
45
+ MIT
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hook-commandlog",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that logs every bash command Claude runs to a log file",
5
+ "type": "module",
6
+ "main": "./src/hook.ts",
7
+ "scripts": {
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "author": "Hasna",
11
+ "license": "MIT"
12
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: commandlog
5
+ *
6
+ * PostToolUse hook that logs every bash command Claude runs to
7
+ * .claude/commands.log in the project directory.
8
+ *
9
+ * Format: [ISO timestamp] <exit_code> <command>
10
+ * One command per line.
11
+ */
12
+
13
+ import { readFileSync, existsSync, mkdirSync, appendFileSync } from "fs";
14
+ import { join } from "path";
15
+
16
+ interface HookInput {
17
+ session_id: string;
18
+ cwd: string;
19
+ tool_name: string;
20
+ tool_input: Record<string, unknown>;
21
+ }
22
+
23
+ interface HookOutput {
24
+ continue: boolean;
25
+ }
26
+
27
+ function readStdinJson(): HookInput | null {
28
+ try {
29
+ const input = readFileSync(0, "utf-8").trim();
30
+ if (!input) return null;
31
+ return JSON.parse(input);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function respond(output: HookOutput): void {
38
+ console.log(JSON.stringify(output));
39
+ }
40
+
41
+ function logCommand(input: HookInput): void {
42
+ const claudeDir = join(input.cwd, ".claude");
43
+
44
+ // Create .claude/ directory if it doesn't exist
45
+ if (!existsSync(claudeDir)) {
46
+ mkdirSync(claudeDir, { recursive: true });
47
+ }
48
+
49
+ const logFile = join(claudeDir, "commands.log");
50
+ const timestamp = new Date().toISOString();
51
+ const command = (input.tool_input.command as string) || "(unknown command)";
52
+ const exitCode = input.tool_input.exit_code;
53
+
54
+ // Format: [timestamp] exit_code command
55
+ // If exit_code is available, include it; otherwise just log the command
56
+ let logLine: string;
57
+ if (exitCode !== undefined && exitCode !== null) {
58
+ logLine = `[${timestamp}] exit=${exitCode} ${command}\n`;
59
+ } else {
60
+ logLine = `[${timestamp}] ${command}\n`;
61
+ }
62
+
63
+ appendFileSync(logFile, logLine);
64
+ }
65
+
66
+ export function run(): void {
67
+ const input = readStdinJson();
68
+
69
+ if (!input) {
70
+ respond({ continue: true });
71
+ return;
72
+ }
73
+
74
+ // Only log Bash tool calls
75
+ if (input.tool_name !== "Bash") {
76
+ respond({ continue: true });
77
+ return;
78
+ }
79
+
80
+ try {
81
+ logCommand(input);
82
+ } catch (error) {
83
+ const errMsg = error instanceof Error ? error.message : String(error);
84
+ console.error(`[hook-commandlog] Warning: failed to log command: ${errMsg}`);
85
+ }
86
+
87
+ respond({ continue: true });
88
+ }
89
+
90
+ if (import.meta.main) {
91
+ run();
92
+ }
@@ -0,0 +1,61 @@
1
+ # hook-costwatch
2
+
3
+ Claude Code hook that estimates session token usage and warns if a budget threshold is exceeded.
4
+
5
+ ## Overview
6
+
7
+ When Claude stops, this hook attempts to estimate the session's token usage by examining the transcript file size. If a budget is configured and the estimated cost exceeds it, a warning is logged to stderr.
8
+
9
+ ## Event
10
+
11
+ - **Stop** (no matcher)
12
+
13
+ ## Configuration
14
+
15
+ ### Budget (optional)
16
+
17
+ Set a per-session budget via environment variable:
18
+
19
+ ```bash
20
+ export COST_WATCH_BUDGET="5.00" # Max $5.00 per session
21
+ ```
22
+
23
+ If not set, the hook runs without budget enforcement and simply logs a reminder to check usage.
24
+
25
+ ## Cost Estimation
26
+
27
+ The estimation is intentionally rough:
28
+
29
+ - **~4 characters per token** (average for English text)
30
+ - **~$30 per million tokens** (blended input/output estimate)
31
+ - Based on transcript file size, not actual API usage
32
+
33
+ This is a ballpark estimate. Always check actual usage at [console.anthropic.com](https://console.anthropic.com/).
34
+
35
+ ## Behavior
36
+
37
+ - Reads the session transcript and estimates total tokens from file size
38
+ - If `COST_WATCH_BUDGET` is set and estimated cost exceeds it, logs a WARNING to stderr
39
+ - If no transcript is found, logs that cost could not be estimated
40
+ - Never blocks the session from ending
41
+ - Outputs `{ "continue": true }` always
42
+
43
+ ## Example Output
44
+
45
+ ```
46
+ [hook-costwatch] Session estimate: ~125.0K tokens, ~$3.75
47
+ [hook-costwatch] Budget: $5.00/session. Remember to check actual usage.
48
+ ```
49
+
50
+ With budget exceeded:
51
+
52
+ ```
53
+ [hook-costwatch] Session estimate: ~250.0K tokens, ~$7.50
54
+ [hook-costwatch] WARNING: Estimated cost ($7.50) exceeds budget ($5.00)!
55
+ [hook-costwatch] Check your actual usage at https://console.anthropic.com/
56
+ [hook-costwatch] Budget: $5.00/session. Remember to check actual usage.
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hook-costwatch",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that estimates session cost and warns if budget is exceeded",
5
+ "type": "module",
6
+ "main": "./src/hook.ts",
7
+ "scripts": {
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "author": "Hasna",
11
+ "license": "MIT"
12
+ }
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: costwatch
5
+ *
6
+ * Stop hook that estimates session token usage and warns if a budget
7
+ * threshold is exceeded.
8
+ *
9
+ * Configuration:
10
+ * - Environment variable: COST_WATCH_BUDGET (max $ per session, e.g. "5.00")
11
+ * - If not set, no budget enforcement (just logs a reminder)
12
+ *
13
+ * Token estimation is rough:
14
+ * - ~4 characters per token (English text average)
15
+ * - Claude Opus pricing: ~$15/M input tokens, ~$75/M output tokens
16
+ * - We estimate a blended rate of ~$30/M tokens for simplicity
17
+ *
18
+ * Since the Stop event provides limited session info, this hook
19
+ * primarily serves as a reminder to check actual usage.
20
+ */
21
+
22
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
23
+ import { join } from "path";
24
+
25
+ interface HookInput {
26
+ session_id: string;
27
+ cwd: string;
28
+ hook_event_name: string;
29
+ transcript_path?: string;
30
+ }
31
+
32
+ interface HookOutput {
33
+ continue: boolean;
34
+ }
35
+
36
+ /** Approximate cost per million tokens (blended input/output estimate) */
37
+ const BLENDED_COST_PER_MILLION_TOKENS = 30;
38
+
39
+ /** Average characters per token */
40
+ const CHARS_PER_TOKEN = 4;
41
+
42
+ function readStdinJson(): HookInput | null {
43
+ try {
44
+ const input = readFileSync(0, "utf-8").trim();
45
+ if (!input) return null;
46
+ return JSON.parse(input);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function respond(output: HookOutput): void {
53
+ console.log(JSON.stringify(output));
54
+ }
55
+
56
+ function getBudget(): number | null {
57
+ const budgetStr = process.env.COST_WATCH_BUDGET;
58
+ if (!budgetStr) return null;
59
+
60
+ const budget = parseFloat(budgetStr);
61
+ if (isNaN(budget) || budget <= 0) {
62
+ console.error(
63
+ `[hook-costwatch] Invalid COST_WATCH_BUDGET value: "${budgetStr}". Must be a positive number.`
64
+ );
65
+ return null;
66
+ }
67
+ return budget;
68
+ }
69
+
70
+ function estimateTranscriptCost(transcriptPath: string): {
71
+ charCount: number;
72
+ estimatedTokens: number;
73
+ estimatedCost: number;
74
+ } | null {
75
+ try {
76
+ if (!existsSync(transcriptPath)) return null;
77
+
78
+ const stat = statSync(transcriptPath);
79
+ const charCount = stat.size;
80
+ const estimatedTokens = Math.ceil(charCount / CHARS_PER_TOKEN);
81
+ const estimatedCost = (estimatedTokens / 1_000_000) * BLENDED_COST_PER_MILLION_TOKENS;
82
+
83
+ return { charCount, estimatedTokens, estimatedCost };
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function findSessionTranscript(cwd: string, sessionId: string): string | null {
90
+ // Check common transcript locations
91
+ const possibleDirs = [
92
+ join(cwd, ".claude"),
93
+ join(process.env.HOME || "", ".claude", "projects"),
94
+ ];
95
+
96
+ for (const dir of possibleDirs) {
97
+ if (!existsSync(dir)) continue;
98
+ try {
99
+ const files = readdirSync(dir, { recursive: true }) as string[];
100
+ for (const file of files) {
101
+ const filePath = join(dir, file);
102
+ if (typeof file === "string" && file.includes(sessionId)) {
103
+ return filePath;
104
+ }
105
+ }
106
+ } catch {
107
+ // Directory not readable, skip
108
+ }
109
+ }
110
+
111
+ return null;
112
+ }
113
+
114
+ export function run(): void {
115
+ const input = readStdinJson();
116
+
117
+ if (!input) {
118
+ respond({ continue: true });
119
+ return;
120
+ }
121
+
122
+ const budget = getBudget();
123
+
124
+ // Try to estimate cost from transcript
125
+ let estimate: {
126
+ charCount: number;
127
+ estimatedTokens: number;
128
+ estimatedCost: number;
129
+ } | null = null;
130
+
131
+ if (input.transcript_path) {
132
+ estimate = estimateTranscriptCost(input.transcript_path);
133
+ }
134
+
135
+ if (!estimate) {
136
+ // Try to find transcript by session ID
137
+ const transcriptPath = findSessionTranscript(input.cwd, input.session_id);
138
+ if (transcriptPath) {
139
+ estimate = estimateTranscriptCost(transcriptPath);
140
+ }
141
+ }
142
+
143
+ if (estimate) {
144
+ const costStr = estimate.estimatedCost.toFixed(2);
145
+ const tokensStr = (estimate.estimatedTokens / 1000).toFixed(1);
146
+
147
+ console.error(`[hook-costwatch] Session estimate: ~${tokensStr}K tokens, ~$${costStr}`);
148
+
149
+ if (budget !== null && estimate.estimatedCost > budget) {
150
+ console.error(
151
+ `[hook-costwatch] WARNING: Estimated cost ($${costStr}) exceeds budget ($${budget.toFixed(2)})!`
152
+ );
153
+ console.error(
154
+ `[hook-costwatch] Check your actual usage at https://console.anthropic.com/`
155
+ );
156
+ }
157
+ } else {
158
+ console.error(
159
+ `[hook-costwatch] Could not estimate session cost (no transcript found).`
160
+ );
161
+ }
162
+
163
+ if (budget !== null) {
164
+ console.error(
165
+ `[hook-costwatch] Budget: $${budget.toFixed(2)}/session. Remember to check actual usage.`
166
+ );
167
+ } else {
168
+ console.error(
169
+ `[hook-costwatch] No budget set. Set COST_WATCH_BUDGET env var to enable budget warnings.`
170
+ );
171
+ }
172
+
173
+ respond({ continue: true });
174
+ }
175
+
176
+ if (import.meta.main) {
177
+ run();
178
+ }
@@ -0,0 +1,50 @@
1
+ # hook-desktopnotify
2
+
3
+ Claude Code hook that sends native desktop notifications when Claude stops.
4
+
5
+ ## Overview
6
+
7
+ Get notified immediately when Claude finishes a task. Uses your OS's native notification system — no external services or accounts needed.
8
+
9
+ ## Platform Support
10
+
11
+ | Platform | Method | Requirements |
12
+ |----------|--------|--------------|
13
+ | macOS | `osascript` (display notification) | None (built-in) |
14
+ | Linux | `notify-send` | `libnotify` package |
15
+
16
+ ## Hook Event
17
+
18
+ - **Stop** (no matcher)
19
+
20
+ ## Behavior
21
+
22
+ 1. Fires when Claude stops and waits for input
23
+ 2. Detects the current platform (`process.platform`)
24
+ 3. Sends a native notification with the project name
25
+ 4. Plays a sound on macOS (Glass)
26
+ 5. Outputs `{ continue: true }`
27
+
28
+ ## Notification Content
29
+
30
+ - **Title**: "Claude Code — Done"
31
+ - **Body**: "Claude has finished working on {project} and is waiting for your input."
32
+
33
+ ## Linux Setup
34
+
35
+ If `notify-send` is not installed:
36
+
37
+ ```bash
38
+ # Ubuntu/Debian
39
+ sudo apt install libnotify-bin
40
+
41
+ # Fedora
42
+ sudo dnf install libnotify
43
+
44
+ # Arch
45
+ sudo pacman -S libnotify
46
+ ```
47
+
48
+ ## License
49
+
50
+ MIT