@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.
- package/.npmrc.example +2 -0
- package/AGENTS.md +54 -0
- package/CLAUDE.md +70 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +232 -0
- package/bin/index.js +5171 -0
- package/hooks/hook-agentmessages/CLAUDE.md +79 -0
- package/hooks/hook-agentmessages/LICENSE +21 -0
- package/hooks/hook-agentmessages/README.md +107 -0
- package/hooks/hook-agentmessages/package.json +31 -0
- package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
- package/hooks/hook-agentmessages/src/install.ts +126 -0
- package/hooks/hook-agentmessages/src/session-start.ts +255 -0
- package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
- package/hooks/hook-branchprotect/CLAUDE.md +23 -0
- package/hooks/hook-branchprotect/README.md +25 -0
- package/hooks/hook-branchprotect/package.json +42 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-branchprotect/tsconfig.json +25 -0
- package/hooks/hook-checkbugs/LICENSE +21 -0
- package/hooks/hook-checkbugs/README.md +140 -0
- package/hooks/hook-checkbugs/package.json +58 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkbugs/tsconfig.json +15 -0
- package/hooks/hook-checkdocs/README.md +137 -0
- package/hooks/hook-checkdocs/package.json +57 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkdocs/tsconfig.json +15 -0
- package/hooks/hook-checkfiles/LICENSE +21 -0
- package/hooks/hook-checkfiles/README.md +141 -0
- package/hooks/hook-checkfiles/package.json +56 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checkfiles/tsconfig.json +15 -0
- package/hooks/hook-checklint/LICENSE +21 -0
- package/hooks/hook-checklint/README.md +147 -0
- package/hooks/hook-checklint/package.json +57 -0
- package/hooks/hook-checklint/src/cli-patch.ts +32 -0
- package/hooks/hook-checklint/src/cli.ts +667 -0
- package/hooks/hook-checklint/src/hook.ts +473 -0
- package/hooks/hook-checklint/tsconfig.json +15 -0
- package/hooks/hook-checkpoint/CLAUDE.md +23 -0
- package/hooks/hook-checkpoint/README.md +37 -0
- package/hooks/hook-checkpoint/package.json +58 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checkpoint/tsconfig.json +25 -0
- package/hooks/hook-checksecurity/LICENSE +21 -0
- package/hooks/hook-checksecurity/README.md +158 -0
- package/hooks/hook-checksecurity/package.json +57 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checksecurity/tsconfig.json +15 -0
- package/hooks/hook-checktasks/README.md +144 -0
- package/hooks/hook-checktasks/package.json +55 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktasks/tsconfig.json +20 -0
- package/hooks/hook-checktests/LICENSE +21 -0
- package/hooks/hook-checktests/README.md +137 -0
- package/hooks/hook-checktests/package.json +57 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-checktests/tsconfig.json +15 -0
- package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
- package/hooks/hook-contextrefresh/README.md +42 -0
- package/hooks/hook-contextrefresh/package.json +42 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-contextrefresh/tsconfig.json +25 -0
- package/hooks/hook-gitguard/CLAUDE.md +22 -0
- package/hooks/hook-gitguard/README.md +30 -0
- package/hooks/hook-gitguard/package.json +57 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-gitguard/tsconfig.json +25 -0
- package/hooks/hook-packageage/CLAUDE.md +23 -0
- package/hooks/hook-packageage/README.md +33 -0
- package/hooks/hook-packageage/package.json +42 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-packageage/tsconfig.json +25 -0
- package/hooks/hook-phonenotify/CLAUDE.md +25 -0
- package/hooks/hook-phonenotify/README.md +44 -0
- package/hooks/hook-phonenotify/package.json +42 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-phonenotify/tsconfig.json +25 -0
- package/hooks/hook-precompact/CLAUDE.md +23 -0
- package/hooks/hook-precompact/README.md +36 -0
- package/hooks/hook-precompact/package.json +42 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/hooks/hook-precompact/tsconfig.json +25 -0
- package/package.json +61 -0
- package/src/cli/components/App.tsx +191 -0
- package/src/cli/components/CategorySelect.tsx +37 -0
- package/src/cli/components/DataTable.tsx +133 -0
- package/src/cli/components/Header.tsx +18 -0
- package/src/cli/components/HookSelect.tsx +29 -0
- package/src/cli/components/InstallProgress.tsx +105 -0
- package/src/cli/components/SearchView.tsx +86 -0
- package/src/cli/index.tsx +218 -0
- package/src/index.ts +31 -0
- package/src/lib/installer.ts +288 -0
- package/src/lib/registry.ts +205 -0
- 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
|
+
}
|