@hasna/hooks 0.0.1

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 (110) hide show
  1. package/.npmrc.example +2 -0
  2. package/AGENTS.md +54 -0
  3. package/CLAUDE.md +70 -0
  4. package/CONTRIBUTING.md +45 -0
  5. package/README.md +232 -0
  6. package/bin/index.js +5171 -0
  7. package/hooks/hook-agentmessages/CLAUDE.md +79 -0
  8. package/hooks/hook-agentmessages/LICENSE +21 -0
  9. package/hooks/hook-agentmessages/README.md +107 -0
  10. package/hooks/hook-agentmessages/package.json +31 -0
  11. package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
  12. package/hooks/hook-agentmessages/src/install.ts +126 -0
  13. package/hooks/hook-agentmessages/src/session-start.ts +255 -0
  14. package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
  15. package/hooks/hook-branchprotect/CLAUDE.md +23 -0
  16. package/hooks/hook-branchprotect/README.md +25 -0
  17. package/hooks/hook-branchprotect/package.json +42 -0
  18. package/hooks/hook-branchprotect/src/cli.ts +126 -0
  19. package/hooks/hook-branchprotect/src/hook.ts +88 -0
  20. package/hooks/hook-branchprotect/tsconfig.json +25 -0
  21. package/hooks/hook-checkbugs/LICENSE +21 -0
  22. package/hooks/hook-checkbugs/README.md +140 -0
  23. package/hooks/hook-checkbugs/package.json +58 -0
  24. package/hooks/hook-checkbugs/src/cli.ts +628 -0
  25. package/hooks/hook-checkbugs/src/hook.ts +335 -0
  26. package/hooks/hook-checkbugs/tsconfig.json +15 -0
  27. package/hooks/hook-checkdocs/README.md +137 -0
  28. package/hooks/hook-checkdocs/package.json +57 -0
  29. package/hooks/hook-checkdocs/src/cli.ts +628 -0
  30. package/hooks/hook-checkdocs/src/hook.ts +310 -0
  31. package/hooks/hook-checkdocs/tsconfig.json +15 -0
  32. package/hooks/hook-checkfiles/LICENSE +21 -0
  33. package/hooks/hook-checkfiles/README.md +141 -0
  34. package/hooks/hook-checkfiles/package.json +56 -0
  35. package/hooks/hook-checkfiles/src/cli.ts +545 -0
  36. package/hooks/hook-checkfiles/src/hook.ts +321 -0
  37. package/hooks/hook-checkfiles/tsconfig.json +15 -0
  38. package/hooks/hook-checklint/LICENSE +21 -0
  39. package/hooks/hook-checklint/README.md +147 -0
  40. package/hooks/hook-checklint/package.json +57 -0
  41. package/hooks/hook-checklint/src/cli-patch.ts +32 -0
  42. package/hooks/hook-checklint/src/cli.ts +667 -0
  43. package/hooks/hook-checklint/src/hook.ts +473 -0
  44. package/hooks/hook-checklint/tsconfig.json +15 -0
  45. package/hooks/hook-checkpoint/CLAUDE.md +23 -0
  46. package/hooks/hook-checkpoint/README.md +37 -0
  47. package/hooks/hook-checkpoint/package.json +58 -0
  48. package/hooks/hook-checkpoint/src/cli.ts +191 -0
  49. package/hooks/hook-checkpoint/src/hook.ts +207 -0
  50. package/hooks/hook-checkpoint/tsconfig.json +25 -0
  51. package/hooks/hook-checksecurity/LICENSE +21 -0
  52. package/hooks/hook-checksecurity/README.md +158 -0
  53. package/hooks/hook-checksecurity/package.json +57 -0
  54. package/hooks/hook-checksecurity/src/cli.ts +601 -0
  55. package/hooks/hook-checksecurity/src/hook.ts +334 -0
  56. package/hooks/hook-checksecurity/tsconfig.json +15 -0
  57. package/hooks/hook-checktasks/README.md +144 -0
  58. package/hooks/hook-checktasks/package.json +55 -0
  59. package/hooks/hook-checktasks/src/cli.ts +578 -0
  60. package/hooks/hook-checktasks/src/hook.ts +308 -0
  61. package/hooks/hook-checktasks/tsconfig.json +20 -0
  62. package/hooks/hook-checktests/LICENSE +21 -0
  63. package/hooks/hook-checktests/README.md +137 -0
  64. package/hooks/hook-checktests/package.json +57 -0
  65. package/hooks/hook-checktests/src/cli.ts +627 -0
  66. package/hooks/hook-checktests/src/hook.ts +334 -0
  67. package/hooks/hook-checktests/tsconfig.json +15 -0
  68. package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
  69. package/hooks/hook-contextrefresh/README.md +42 -0
  70. package/hooks/hook-contextrefresh/package.json +42 -0
  71. package/hooks/hook-contextrefresh/src/cli.ts +152 -0
  72. package/hooks/hook-contextrefresh/src/hook.ts +148 -0
  73. package/hooks/hook-contextrefresh/tsconfig.json +25 -0
  74. package/hooks/hook-gitguard/CLAUDE.md +22 -0
  75. package/hooks/hook-gitguard/README.md +30 -0
  76. package/hooks/hook-gitguard/package.json +57 -0
  77. package/hooks/hook-gitguard/src/cli.ts +159 -0
  78. package/hooks/hook-gitguard/src/hook.ts +129 -0
  79. package/hooks/hook-gitguard/tsconfig.json +25 -0
  80. package/hooks/hook-packageage/CLAUDE.md +23 -0
  81. package/hooks/hook-packageage/README.md +33 -0
  82. package/hooks/hook-packageage/package.json +42 -0
  83. package/hooks/hook-packageage/src/cli.ts +165 -0
  84. package/hooks/hook-packageage/src/hook.ts +177 -0
  85. package/hooks/hook-packageage/tsconfig.json +25 -0
  86. package/hooks/hook-phonenotify/CLAUDE.md +25 -0
  87. package/hooks/hook-phonenotify/README.md +44 -0
  88. package/hooks/hook-phonenotify/package.json +42 -0
  89. package/hooks/hook-phonenotify/src/cli.ts +196 -0
  90. package/hooks/hook-phonenotify/src/hook.ts +139 -0
  91. package/hooks/hook-phonenotify/tsconfig.json +25 -0
  92. package/hooks/hook-precompact/CLAUDE.md +23 -0
  93. package/hooks/hook-precompact/README.md +36 -0
  94. package/hooks/hook-precompact/package.json +42 -0
  95. package/hooks/hook-precompact/src/cli.ts +168 -0
  96. package/hooks/hook-precompact/src/hook.ts +122 -0
  97. package/hooks/hook-precompact/tsconfig.json +25 -0
  98. package/package.json +61 -0
  99. package/src/cli/components/App.tsx +191 -0
  100. package/src/cli/components/CategorySelect.tsx +37 -0
  101. package/src/cli/components/DataTable.tsx +133 -0
  102. package/src/cli/components/Header.tsx +18 -0
  103. package/src/cli/components/HookSelect.tsx +29 -0
  104. package/src/cli/components/InstallProgress.tsx +105 -0
  105. package/src/cli/components/SearchView.tsx +86 -0
  106. package/src/cli/index.tsx +218 -0
  107. package/src/index.ts +31 -0
  108. package/src/lib/installer.ts +288 -0
  109. package/src/lib/registry.ts +205 -0
  110. package/tsconfig.json +17 -0
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: contextrefresh
5
+ *
6
+ * UserPromptSubmit hook that re-injects important context every N prompts
7
+ * to prevent context decay in long sessions.
8
+ *
9
+ * Reads context from .claude-context file in project root and injects
10
+ * it as a system message every N user prompts. Tracks prompt count
11
+ * in a persistent counter file.
12
+ */
13
+
14
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
15
+ import { join } from "path";
16
+ import { homedir } from "os";
17
+ import { tmpdir } from "os";
18
+
19
+ interface HookInput {
20
+ session_id: string;
21
+ cwd: string;
22
+ hook_event_name: string;
23
+ user_prompt?: string;
24
+ }
25
+
26
+ interface HookOutput {
27
+ continue?: boolean;
28
+ suppressPrompt?: boolean;
29
+ updatedPrompt?: string;
30
+ }
31
+
32
+ interface RefreshConfig {
33
+ enabled?: boolean;
34
+ interval?: number;
35
+ contextFile?: string;
36
+ }
37
+
38
+ const CONFIG_KEY = "contextRefreshConfig";
39
+ const COUNTER_DIR = join(tmpdir(), "hook-contextrefresh");
40
+
41
+ function readStdinJson(): HookInput | null {
42
+ try {
43
+ const input = readFileSync(0, "utf-8").trim();
44
+ if (!input) return null;
45
+ return JSON.parse(input);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function getConfig(): RefreshConfig {
52
+ const settingsPath = join(homedir(), ".claude", "settings.json");
53
+ try {
54
+ if (existsSync(settingsPath)) {
55
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
56
+ return settings[CONFIG_KEY] || { enabled: true, interval: 10 };
57
+ }
58
+ } catch {}
59
+ return { enabled: true, interval: 10 };
60
+ }
61
+
62
+ function getPromptCount(sessionId: string): number {
63
+ mkdirSync(COUNTER_DIR, { recursive: true });
64
+ const counterFile = join(COUNTER_DIR, `${sessionId}.count`);
65
+ try {
66
+ if (existsSync(counterFile)) {
67
+ return parseInt(readFileSync(counterFile, "utf-8").trim(), 10) || 0;
68
+ }
69
+ } catch {}
70
+ return 0;
71
+ }
72
+
73
+ function setPromptCount(sessionId: string, count: number): void {
74
+ mkdirSync(COUNTER_DIR, { recursive: true });
75
+ const counterFile = join(COUNTER_DIR, `${sessionId}.count`);
76
+ writeFileSync(counterFile, String(count));
77
+ }
78
+
79
+ function getContextContent(cwd: string, contextFile?: string): string | null {
80
+ // Try configured context file, then defaults
81
+ const candidates = [
82
+ contextFile ? join(cwd, contextFile) : null,
83
+ join(cwd, ".claude-context"),
84
+ join(cwd, ".claude-refresh"),
85
+ ].filter(Boolean) as string[];
86
+
87
+ for (const path of candidates) {
88
+ if (existsSync(path)) {
89
+ try {
90
+ return readFileSync(path, "utf-8").trim();
91
+ } catch {}
92
+ }
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ function respond(output: HookOutput): void {
99
+ console.log(JSON.stringify(output));
100
+ }
101
+
102
+ export function run(): void {
103
+ const input = readStdinJson();
104
+
105
+ if (!input) {
106
+ respond({ continue: true });
107
+ return;
108
+ }
109
+
110
+ const config = getConfig();
111
+
112
+ if (!config.enabled) {
113
+ respond({ continue: true });
114
+ return;
115
+ }
116
+
117
+ const interval = config.interval || 10;
118
+ const count = getPromptCount(input.session_id) + 1;
119
+ setPromptCount(input.session_id, count);
120
+
121
+ // Check if it's time to inject context
122
+ if (count % interval !== 0) {
123
+ respond({ continue: true });
124
+ return;
125
+ }
126
+
127
+ const contextContent = getContextContent(input.cwd, config.contextFile);
128
+
129
+ if (!contextContent) {
130
+ respond({ continue: true });
131
+ return;
132
+ }
133
+
134
+ // Inject context by prepending it to the user's prompt
135
+ const refreshPrefix = `[Context Refresh - Prompt #${count}]\n${contextContent}\n\n---\n\n`;
136
+ const updatedPrompt = input.user_prompt
137
+ ? `${refreshPrefix}${input.user_prompt}`
138
+ : undefined;
139
+
140
+ respond({
141
+ continue: true,
142
+ updatedPrompt,
143
+ });
144
+ }
145
+
146
+ if (import.meta.main) {
147
+ run();
148
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "lib": ["ESNext"],
6
+ "moduleResolution": "bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "declaration": true,
18
+ "declarationMap": true,
19
+ "outDir": "./dist",
20
+ "rootDir": "./src",
21
+ "types": ["bun-types"]
22
+ },
23
+ "include": ["src/**/*"],
24
+ "exclude": ["node_modules", "dist"]
25
+ }
@@ -0,0 +1,22 @@
1
+ # CLAUDE.md
2
+
3
+ ## hook-gitguard
4
+
5
+ A PreToolUse hook that blocks destructive git operations.
6
+
7
+ ### Key Files
8
+
9
+ | File | Purpose |
10
+ |------|---------|
11
+ | `src/hook.ts` | Main hook logic — pattern matching against dangerous git commands |
12
+ | `src/cli.ts` | CLI — install/uninstall/status/test |
13
+
14
+ ### Hook Events
15
+
16
+ - **PreToolUse** (matcher: `Bash`)
17
+
18
+ ### Behavior
19
+
20
+ - Checks Bash commands for destructive git patterns
21
+ - Blocks: reset --hard, push --force, checkout ., clean -f, branch -D, stash drop/clear
22
+ - Approves all non-git commands immediately
@@ -0,0 +1,30 @@
1
+ # hook-gitguard
2
+
3
+ Claude Code hook that blocks destructive git operations.
4
+
5
+ ## Overview
6
+
7
+ Prevents Claude from running irreversible git commands like `git reset --hard`, `git push --force`, `git checkout .`, `git clean -f`, and more.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun install -g @hasnaxyz/hook-gitguard
13
+ hook-gitguard install
14
+ ```
15
+
16
+ ## Blocked Operations
17
+
18
+ - `git reset --hard` — discards all uncommitted changes
19
+ - `git push --force` / `-f` — overwrites remote history
20
+ - `git checkout .` / `git checkout -- .` — discards working directory
21
+ - `git restore .` — discards working directory changes
22
+ - `git clean -f` — removes untracked files permanently
23
+ - `git branch -D` — force deletes branch without merge check
24
+ - `git stash drop` / `clear` — permanently deletes stash entries
25
+ - `git reflog expire` / `delete` — destroys recovery points
26
+ - `git gc --prune=now` — permanently removes unreachable objects
27
+
28
+ ## License
29
+
30
+ MIT
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@hasnaxyz/hook-gitguard",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that blocks destructive git operations",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-gitguard": "./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/cli.ts ./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
+ "git",
33
+ "safety",
34
+ "guard",
35
+ "destructive",
36
+ "cli"
37
+ ],
38
+ "author": "Hasna",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/hasnaxyz/hook-gitguard.git"
43
+ },
44
+ "publishConfig": {
45
+ "access": "restricted",
46
+ "registry": "https://registry.npmjs.org/"
47
+ },
48
+ "engines": {
49
+ "node": ">=18",
50
+ "bun": ">=1.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/bun": "^1.3.8",
54
+ "@types/node": "^20",
55
+ "typescript": "^5.0.0"
56
+ }
57
+ }
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * CLI for hook-gitguard
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+
11
+ const HOOK_NAME = "hook-gitguard";
12
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
13
+
14
+ interface ClaudeSettings {
15
+ hooks?: {
16
+ PreToolUse?: Array<{
17
+ matcher: string;
18
+ hooks: Array<{ type: "command"; command: string }>;
19
+ }>;
20
+ };
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ function readSettings(): ClaudeSettings {
25
+ try {
26
+ if (existsSync(SETTINGS_PATH)) {
27
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
28
+ }
29
+ } catch {}
30
+ return {};
31
+ }
32
+
33
+ function writeSettings(settings: ClaudeSettings): void {
34
+ const dir = join(homedir(), ".claude");
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
37
+ }
38
+
39
+ function install(): void {
40
+ const settings = readSettings();
41
+ if (!settings.hooks) settings.hooks = {};
42
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
43
+
44
+ const existing = settings.hooks.PreToolUse.find((h) =>
45
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
46
+ );
47
+
48
+ if (existing) {
49
+ console.log(`${HOOK_NAME} is already installed`);
50
+ return;
51
+ }
52
+
53
+ settings.hooks.PreToolUse.push({
54
+ matcher: "Bash",
55
+ hooks: [{ type: "command", command: `bunx @hasnaxyz/${HOOK_NAME}` }],
56
+ });
57
+
58
+ writeSettings(settings);
59
+ console.log(`${HOOK_NAME} installed successfully`);
60
+ console.log("Hook will block destructive git operations");
61
+ }
62
+
63
+ function uninstall(): void {
64
+ const settings = readSettings();
65
+ if (!settings.hooks?.PreToolUse) {
66
+ console.log(`${HOOK_NAME} is not installed`);
67
+ return;
68
+ }
69
+
70
+ const before = settings.hooks.PreToolUse.length;
71
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
72
+ (h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
73
+ );
74
+
75
+ if (before === settings.hooks.PreToolUse.length) {
76
+ console.log(`${HOOK_NAME} is not installed`);
77
+ return;
78
+ }
79
+
80
+ writeSettings(settings);
81
+ console.log(`${HOOK_NAME} uninstalled successfully`);
82
+ }
83
+
84
+ function status(): void {
85
+ const settings = readSettings();
86
+ const installed = settings.hooks?.PreToolUse?.some((h) =>
87
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
88
+ );
89
+ console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
90
+ }
91
+
92
+ function test(command: string): void {
93
+ const PATTERNS: Array<{ pattern: RegExp; description: string }> = [
94
+ { pattern: /git\s+reset\s+--hard/, description: "git reset --hard" },
95
+ { pattern: /git\s+push\s+.*--force(?!-)/, description: "git push --force" },
96
+ { pattern: /git\s+push\s+.*\s-f\b/, description: "git push -f" },
97
+ { pattern: /git\s+checkout\s+\.\s*$/, description: "git checkout ." },
98
+ { pattern: /git\s+checkout\s+--\s+\./, description: "git checkout -- ." },
99
+ { pattern: /git\s+clean\s+(-[a-zA-Z]*f|--force)/, description: "git clean -f" },
100
+ { pattern: /git\s+branch\s+-D\s/, description: "git branch -D" },
101
+ { pattern: /git\s+stash\s+(drop|clear)/, description: "git stash drop/clear" },
102
+ ];
103
+
104
+ for (const { pattern, description } of PATTERNS) {
105
+ if (pattern.test(command)) {
106
+ console.log(`BLOCKED: ${description}`);
107
+ return;
108
+ }
109
+ }
110
+ console.log("ALLOWED");
111
+ }
112
+
113
+ function help(): void {
114
+ console.log(`
115
+ ${HOOK_NAME} - Block destructive git operations in Claude Code
116
+
117
+ Usage: ${HOOK_NAME} <command>
118
+
119
+ Commands:
120
+ install Install hook to Claude Code settings
121
+ uninstall Remove hook from Claude Code settings
122
+ status Check if hook is installed
123
+ test <cmd> Test if a git command would be blocked
124
+ help Show this help message
125
+
126
+ Blocked operations:
127
+ git reset --hard git push --force / -f
128
+ git checkout . git checkout -- .
129
+ git clean -f git branch -D
130
+ git stash drop/clear git reflog expire/delete
131
+ `);
132
+ }
133
+
134
+ const command = process.argv[2];
135
+
136
+ switch (command) {
137
+ case "install": install(); break;
138
+ case "uninstall": uninstall(); break;
139
+ case "status": status(); break;
140
+ case "test":
141
+ if (process.argv[3]) {
142
+ test(process.argv.slice(3).join(" "));
143
+ } else {
144
+ console.error("Usage: hook-gitguard test <command>");
145
+ process.exit(1);
146
+ }
147
+ break;
148
+ case "help":
149
+ case "--help":
150
+ case "-h": help(); break;
151
+ default:
152
+ if (!command) {
153
+ import("./hook.ts").then((m) => m.run());
154
+ } else {
155
+ console.error(`Unknown command: ${command}`);
156
+ help();
157
+ process.exit(1);
158
+ }
159
+ }
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: gitguard
5
+ *
6
+ * PreToolUse hook that blocks destructive git operations:
7
+ * - git reset --hard
8
+ * - git push --force / -f (especially to main/master)
9
+ * - git checkout . / git checkout -- .
10
+ * - git clean -f / -fd
11
+ * - git branch -D (force delete)
12
+ * - git stash drop / clear
13
+ * - git rebase without caution
14
+ */
15
+
16
+ import { readFileSync } from "fs";
17
+
18
+ interface HookInput {
19
+ session_id: string;
20
+ cwd: string;
21
+ tool_name: string;
22
+ tool_input: Record<string, unknown>;
23
+ }
24
+
25
+ interface HookOutput {
26
+ decision?: "approve" | "block";
27
+ reason?: string;
28
+ }
29
+
30
+ const DESTRUCTIVE_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
31
+ // git reset --hard
32
+ { pattern: /git\s+reset\s+--hard/, description: "git reset --hard (discards all uncommitted changes)" },
33
+
34
+ // git push --force (any variant)
35
+ { pattern: /git\s+push\s+.*--force-with-lease/, description: "git push --force-with-lease (force push with safety)" },
36
+ { pattern: /git\s+push\s+.*--force(?!-)/, description: "git push --force (overwrites remote history)" },
37
+ { pattern: /git\s+push\s+.*\s-f\b/, description: "git push -f (force push)" },
38
+
39
+ // Force push to main/master specifically
40
+ { pattern: /git\s+push\s+.*--force.*\s+(main|master)\b/, description: "force push to main/master" },
41
+ { pattern: /git\s+push\s+.*-f\s+.*(main|master)\b/, description: "force push to main/master" },
42
+
43
+ // git checkout . / git checkout -- . (discard all changes)
44
+ { pattern: /git\s+checkout\s+\.\s*$/, description: "git checkout . (discards all working directory changes)" },
45
+ { pattern: /git\s+checkout\s+--\s+\./, description: "git checkout -- . (discards all working directory changes)" },
46
+
47
+ // git restore . (discard all changes)
48
+ { pattern: /git\s+restore\s+\.\s*$/, description: "git restore . (discards all working directory changes)" },
49
+ { pattern: /git\s+restore\s+--staged\s+--worktree\s+\./, description: "git restore --staged --worktree . (discards everything)" },
50
+
51
+ // git clean -f (remove untracked files)
52
+ { pattern: /git\s+clean\s+(-[a-zA-Z]*f|--force)/, description: "git clean -f (removes untracked files permanently)" },
53
+
54
+ // git branch -D (force delete branch)
55
+ { pattern: /git\s+branch\s+-D\s/, description: "git branch -D (force delete branch without merge check)" },
56
+
57
+ // git stash drop/clear
58
+ { pattern: /git\s+stash\s+drop/, description: "git stash drop (permanently deletes stash entry)" },
59
+ { pattern: /git\s+stash\s+clear/, description: "git stash clear (deletes all stash entries)" },
60
+
61
+ // git reflog expire/delete
62
+ { pattern: /git\s+reflog\s+(expire|delete)/, description: "git reflog expire/delete (destroys recovery points)" },
63
+
64
+ // git gc --prune=now
65
+ { pattern: /git\s+gc\s+--prune=now/, description: "git gc --prune=now (permanently removes unreachable objects)" },
66
+ ];
67
+
68
+ function readStdinJson(): HookInput | null {
69
+ try {
70
+ const input = readFileSync(0, "utf-8").trim();
71
+ if (!input) return null;
72
+ return JSON.parse(input);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function checkDestructiveGit(command: string): { blocked: boolean; reason?: string } {
79
+ for (const { pattern, description } of DESTRUCTIVE_PATTERNS) {
80
+ if (pattern.test(command)) {
81
+ return { blocked: true, reason: `Blocked: ${description}` };
82
+ }
83
+ }
84
+ return { blocked: false };
85
+ }
86
+
87
+ function respond(output: HookOutput): void {
88
+ console.log(JSON.stringify(output));
89
+ }
90
+
91
+ export function run(): void {
92
+ const input = readStdinJson();
93
+
94
+ if (!input) {
95
+ respond({ decision: "approve" });
96
+ return;
97
+ }
98
+
99
+ if (input.tool_name !== "Bash") {
100
+ respond({ decision: "approve" });
101
+ return;
102
+ }
103
+
104
+ const command = input.tool_input?.command as string;
105
+ if (!command || typeof command !== "string") {
106
+ respond({ decision: "approve" });
107
+ return;
108
+ }
109
+
110
+ // Only check commands that contain "git"
111
+ if (!command.includes("git")) {
112
+ respond({ decision: "approve" });
113
+ return;
114
+ }
115
+
116
+ const result = checkDestructiveGit(command);
117
+
118
+ if (result.blocked) {
119
+ console.error(`[hook-gitguard] ${result.reason}`);
120
+ respond({ decision: "block", reason: result.reason });
121
+ return;
122
+ }
123
+
124
+ respond({ decision: "approve" });
125
+ }
126
+
127
+ if (import.meta.main) {
128
+ run();
129
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "lib": ["ESNext"],
6
+ "moduleResolution": "bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "declaration": true,
18
+ "declarationMap": true,
19
+ "outDir": "./dist",
20
+ "rootDir": "./src",
21
+ "types": ["bun-types"]
22
+ },
23
+ "include": ["src/**/*"],
24
+ "exclude": ["node_modules", "dist"]
25
+ }
@@ -0,0 +1,23 @@
1
+ # CLAUDE.md
2
+
3
+ ## hook-packageage
4
+
5
+ A PreToolUse hook that checks package age before npm/bun install commands.
6
+
7
+ ### Key Files
8
+
9
+ | File | Purpose |
10
+ |------|---------|
11
+ | `src/hook.ts` | Main hook logic — extracts packages from commands, checks npm registry |
12
+ | `src/cli.ts` | CLI — install/uninstall/status/check |
13
+
14
+ ### Hook Events
15
+
16
+ - **PreToolUse** (matcher: `Bash`)
17
+
18
+ ### Behavior
19
+
20
+ - Parses npm/bun/yarn/pnpm install commands to extract package names
21
+ - Checks each package against npm registry for last publish date
22
+ - Warns on stale (>1yr), abandoned (>2yr), or deprecated packages
23
+ - Non-blocking — warns but always approves (packages may still be valid)
@@ -0,0 +1,33 @@
1
+ # hook-packageage
2
+
3
+ Claude Code hook that checks package age before install to warn on outdated or abandoned packages.
4
+
5
+ ## Overview
6
+
7
+ Before `npm install`, `bun add`, `yarn add`, or `pnpm add`, this hook checks each package against the npm registry and warns if packages are stale (>1 year) or potentially abandoned (>2 years).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun install -g @hasnaxyz/hook-packageage
13
+ hook-packageage install
14
+ ```
15
+
16
+ ## Commands
17
+
18
+ ```bash
19
+ hook-packageage install # Install to Claude Code settings
20
+ hook-packageage uninstall # Remove hook
21
+ hook-packageage status # Check installation
22
+ hook-packageage check <pkg> # Manually check a package
23
+ ```
24
+
25
+ ## Thresholds
26
+
27
+ - **>1 year** since last publish: STALE warning
28
+ - **>2 years** since last publish: ABANDONED warning
29
+ - **Deprecated** packages: always warned
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@hasnaxyz/hook-packageage",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that checks package age before install to warn on outdated or abandoned packages",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-packageage": "./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": ["dist", "README.md"],
20
+ "scripts": {
21
+ "build": "bun build ./src/cli.ts ./src/hook.ts --outdir ./dist --target node",
22
+ "prepublishOnly": "bun run build",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "keywords": ["claude-code", "claude", "hook", "package", "age", "outdated", "abandoned", "npm", "cli"],
26
+ "author": "Hasna",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/hasnaxyz/hook-packageage.git"
31
+ },
32
+ "publishConfig": {
33
+ "access": "restricted",
34
+ "registry": "https://registry.npmjs.org/"
35
+ },
36
+ "engines": { "node": ">=18", "bun": ">=1.0" },
37
+ "devDependencies": {
38
+ "@types/bun": "^1.3.8",
39
+ "@types/node": "^20",
40
+ "typescript": "^5.0.0"
41
+ }
42
+ }