@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,191 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * CLI for hook-checkpoint
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8
+ import { execSync } from "child_process";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+
12
+ const HOOK_NAME = "hook-checkpoint";
13
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
14
+ const CHECKPOINT_DIR = ".claude-checkpoints";
15
+
16
+ interface ClaudeSettings {
17
+ hooks?: {
18
+ PreToolUse?: Array<{
19
+ matcher: string;
20
+ hooks: Array<{ type: "command"; command: string }>;
21
+ }>;
22
+ };
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ function readSettings(): ClaudeSettings {
27
+ try {
28
+ if (existsSync(SETTINGS_PATH)) {
29
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
30
+ }
31
+ } catch {}
32
+ return {};
33
+ }
34
+
35
+ function writeSettings(settings: ClaudeSettings): void {
36
+ const dir = join(homedir(), ".claude");
37
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
38
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
39
+ }
40
+
41
+ function install(): void {
42
+ const settings = readSettings();
43
+ if (!settings.hooks) settings.hooks = {};
44
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
45
+
46
+ const existing = settings.hooks.PreToolUse.find((h) =>
47
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
48
+ );
49
+
50
+ if (existing) {
51
+ console.log(`${HOOK_NAME} is already installed`);
52
+ return;
53
+ }
54
+
55
+ settings.hooks.PreToolUse.push({
56
+ matcher: "Write|Edit|NotebookEdit",
57
+ hooks: [{ type: "command", command: `bunx @hasnaxyz/${HOOK_NAME}` }],
58
+ });
59
+
60
+ writeSettings(settings);
61
+ console.log(`${HOOK_NAME} installed successfully`);
62
+ console.log("Hook will create shadow git snapshots before Write/Edit/NotebookEdit");
63
+ }
64
+
65
+ function uninstall(): void {
66
+ const settings = readSettings();
67
+ if (!settings.hooks?.PreToolUse) {
68
+ console.log(`${HOOK_NAME} is not installed`);
69
+ return;
70
+ }
71
+
72
+ const before = settings.hooks.PreToolUse.length;
73
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
74
+ (h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
75
+ );
76
+
77
+ if (before === settings.hooks.PreToolUse.length) {
78
+ console.log(`${HOOK_NAME} is not installed`);
79
+ return;
80
+ }
81
+
82
+ writeSettings(settings);
83
+ console.log(`${HOOK_NAME} uninstalled successfully`);
84
+ }
85
+
86
+ function status(): void {
87
+ const settings = readSettings();
88
+ const installed = settings.hooks?.PreToolUse?.some((h) =>
89
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
90
+ );
91
+ console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
92
+ }
93
+
94
+ function list(): void {
95
+ const checkpointDir = join(process.cwd(), CHECKPOINT_DIR);
96
+ if (!existsSync(checkpointDir)) {
97
+ console.log("No checkpoints found in current directory");
98
+ return;
99
+ }
100
+
101
+ try {
102
+ const log = execSync("git log --oneline -20", {
103
+ cwd: checkpointDir,
104
+ encoding: "utf-8",
105
+ });
106
+ console.log("Recent checkpoints:");
107
+ console.log(log);
108
+ } catch {
109
+ console.log("No checkpoints found");
110
+ }
111
+ }
112
+
113
+ function restore(ref: string): void {
114
+ const checkpointDir = join(process.cwd(), CHECKPOINT_DIR);
115
+ if (!existsSync(checkpointDir)) {
116
+ console.log("No checkpoints found in current directory");
117
+ return;
118
+ }
119
+
120
+ try {
121
+ const files = execSync(`git show ${ref} --name-only --pretty=format:""`, {
122
+ cwd: checkpointDir,
123
+ encoding: "utf-8",
124
+ }).trim().split("\n").filter((f) => f.startsWith("files/"));
125
+
126
+ for (const file of files) {
127
+ const relativePath = file.replace("files/", "");
128
+ try {
129
+ const content = execSync(`git show ${ref}:${file}`, {
130
+ cwd: checkpointDir,
131
+ });
132
+ const targetPath = join(process.cwd(), relativePath);
133
+ writeFileSync(targetPath, content);
134
+ console.log(`Restored: ${relativePath}`);
135
+ } catch {
136
+ console.error(`Failed to restore: ${relativePath}`);
137
+ }
138
+ }
139
+ } catch (error) {
140
+ console.error(`Failed to restore from ${ref}:`, error);
141
+ }
142
+ }
143
+
144
+ function help(): void {
145
+ console.log(`
146
+ ${HOOK_NAME} - Shadow git snapshots for Claude Code file modifications
147
+
148
+ Usage: ${HOOK_NAME} <command>
149
+
150
+ Commands:
151
+ install Install hook to Claude Code settings
152
+ uninstall Remove hook from Claude Code settings
153
+ status Check if hook is installed
154
+ list Show recent checkpoints in current directory
155
+ restore <ref> Restore files from a checkpoint (git ref)
156
+ help Show this help message
157
+
158
+ How it works:
159
+ Before any Write/Edit/NotebookEdit, the hook copies the original file
160
+ into a shadow git repo (.claude-checkpoints/) and commits it. This gives
161
+ you a full history of every file before Claude modified it.
162
+ `);
163
+ }
164
+
165
+ const command = process.argv[2];
166
+
167
+ switch (command) {
168
+ case "install": install(); break;
169
+ case "uninstall": uninstall(); break;
170
+ case "status": status(); break;
171
+ case "list": list(); break;
172
+ case "restore":
173
+ if (process.argv[3]) {
174
+ restore(process.argv[3]);
175
+ } else {
176
+ console.error("Usage: hook-checkpoint restore <git-ref>");
177
+ process.exit(1);
178
+ }
179
+ break;
180
+ case "help":
181
+ case "--help":
182
+ case "-h": help(); break;
183
+ default:
184
+ if (!command) {
185
+ import("./hook.ts").then((m) => m.run());
186
+ } else {
187
+ console.error(`Unknown command: ${command}`);
188
+ help();
189
+ process.exit(1);
190
+ }
191
+ }
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: checkpoint
5
+ *
6
+ * PreToolUse hook that creates shadow git snapshots before any file
7
+ * Write/Edit operations. This provides a safety net with easy rollback
8
+ * capability without cluttering the main project's git history.
9
+ *
10
+ * Shadow repo is stored in .claude-checkpoints/ (gitignored).
11
+ */
12
+
13
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, appendFileSync } from "fs";
14
+ import { execSync } from "child_process";
15
+ import { join, resolve } from "path";
16
+
17
+ interface HookInput {
18
+ session_id: string;
19
+ cwd: string;
20
+ tool_name: string;
21
+ tool_input: Record<string, unknown>;
22
+ }
23
+
24
+ interface HookOutput {
25
+ decision?: "approve" | "block";
26
+ reason?: string;
27
+ }
28
+
29
+ const CHECKPOINT_DIR = ".claude-checkpoints";
30
+ const TOOLS_TO_CHECKPOINT = ["Write", "Edit", "NotebookEdit"];
31
+
32
+ /**
33
+ * Read JSON from stdin
34
+ */
35
+ function readStdinJson(): HookInput | null {
36
+ try {
37
+ const input = readFileSync(0, "utf-8").trim();
38
+ if (!input) return null;
39
+ return JSON.parse(input);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Initialize shadow git repo for checkpoints
47
+ */
48
+ function initShadowRepo(cwd: string): string {
49
+ const shadowPath = join(cwd, CHECKPOINT_DIR);
50
+
51
+ if (!existsSync(shadowPath)) {
52
+ mkdirSync(shadowPath, { recursive: true });
53
+ }
54
+
55
+ const gitDir = join(shadowPath, ".git");
56
+ if (!existsSync(gitDir)) {
57
+ execSync("git init", { cwd: shadowPath, stdio: "pipe" });
58
+ execSync('git config user.email "hook-checkpoint@claude.local"', {
59
+ cwd: shadowPath,
60
+ stdio: "pipe",
61
+ });
62
+ execSync('git config user.name "hook-checkpoint"', {
63
+ cwd: shadowPath,
64
+ stdio: "pipe",
65
+ });
66
+
67
+ // Create initial commit
68
+ writeFileSync(join(shadowPath, ".checkpoint-init"), new Date().toISOString());
69
+ execSync("git add -A && git commit -m 'init: checkpoint repo'", {
70
+ cwd: shadowPath,
71
+ stdio: "pipe",
72
+ });
73
+ }
74
+
75
+ // Ensure .claude-checkpoints is gitignored in main project
76
+ const mainGitignore = join(cwd, ".gitignore");
77
+ if (existsSync(mainGitignore)) {
78
+ const content = readFileSync(mainGitignore, "utf-8");
79
+ if (!content.includes(CHECKPOINT_DIR)) {
80
+ appendFileSync(mainGitignore, `\n${CHECKPOINT_DIR}/\n`);
81
+ }
82
+ }
83
+
84
+ return shadowPath;
85
+ }
86
+
87
+ /**
88
+ * Copy the target file into the shadow repo and commit
89
+ */
90
+ function createCheckpoint(
91
+ cwd: string,
92
+ shadowPath: string,
93
+ toolName: string,
94
+ filePath: string,
95
+ sessionId: string
96
+ ): void {
97
+ const absFilePath = resolve(cwd, filePath);
98
+
99
+ if (!existsSync(absFilePath)) {
100
+ // File doesn't exist yet (new file being created) — log but no snapshot needed
101
+ const logEntry = `[${new Date().toISOString()}] ${toolName} creating new file: ${filePath}\n`;
102
+ appendFileSync(join(shadowPath, "checkpoint.log"), logEntry);
103
+ return;
104
+ }
105
+
106
+ // Copy file to shadow repo preserving relative path
107
+ const relativePath = filePath.startsWith("/")
108
+ ? filePath.replace(cwd, "").replace(/^\//, "")
109
+ : filePath;
110
+ const shadowFilePath = join(shadowPath, "files", relativePath);
111
+ const shadowFileDir = join(shadowFilePath, "..");
112
+
113
+ mkdirSync(shadowFileDir, { recursive: true });
114
+
115
+ const content = readFileSync(absFilePath);
116
+ writeFileSync(shadowFilePath, content);
117
+
118
+ // Create metadata
119
+ const metadata = {
120
+ tool: toolName,
121
+ file: filePath,
122
+ session: sessionId,
123
+ timestamp: new Date().toISOString(),
124
+ };
125
+ writeFileSync(
126
+ join(shadowPath, "last-checkpoint.json"),
127
+ JSON.stringify(metadata, null, 2)
128
+ );
129
+
130
+ // Commit to shadow repo
131
+ try {
132
+ execSync("git add -A", { cwd: shadowPath, stdio: "pipe" });
133
+ const msg = `checkpoint: ${toolName} ${relativePath}`;
134
+ execSync(`git commit -m "${msg}" --allow-empty`, {
135
+ cwd: shadowPath,
136
+ stdio: "pipe",
137
+ });
138
+ } catch {
139
+ // Commit may fail if nothing changed — that's fine
140
+ }
141
+
142
+ // Log
143
+ const logEntry = `[${new Date().toISOString()}] ${toolName} → ${relativePath} (session: ${sessionId})\n`;
144
+ appendFileSync(join(shadowPath, "checkpoint.log"), logEntry);
145
+ }
146
+
147
+ /**
148
+ * Extract file path from tool input
149
+ */
150
+ function getFilePath(toolName: string, toolInput: Record<string, unknown>): string | null {
151
+ switch (toolName) {
152
+ case "Write":
153
+ case "Edit":
154
+ return (toolInput.file_path as string) || null;
155
+ case "NotebookEdit":
156
+ return (toolInput.notebook_path as string) || null;
157
+ default:
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Output hook response
164
+ */
165
+ function respond(output: HookOutput): void {
166
+ console.log(JSON.stringify(output));
167
+ }
168
+
169
+ /**
170
+ * Main hook execution
171
+ */
172
+ export function run(): void {
173
+ const input = readStdinJson();
174
+
175
+ if (!input) {
176
+ respond({ decision: "approve" });
177
+ return;
178
+ }
179
+
180
+ // Only checkpoint file-modifying tools
181
+ if (!TOOLS_TO_CHECKPOINT.includes(input.tool_name)) {
182
+ respond({ decision: "approve" });
183
+ return;
184
+ }
185
+
186
+ const filePath = getFilePath(input.tool_name, input.tool_input);
187
+ if (!filePath) {
188
+ respond({ decision: "approve" });
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const shadowPath = initShadowRepo(input.cwd);
194
+ createCheckpoint(input.cwd, shadowPath, input.tool_name, filePath, input.session_id);
195
+ } catch (error) {
196
+ // Never block operations due to checkpoint failures
197
+ const errMsg = error instanceof Error ? error.message : String(error);
198
+ console.error(`[hook-checkpoint] Warning: checkpoint failed: ${errMsg}`);
199
+ }
200
+
201
+ // Always approve — checkpointing is non-blocking
202
+ respond({ decision: "approve" });
203
+ }
204
+
205
+ if (import.meta.main) {
206
+ run();
207
+ }
@@ -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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hasna
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,158 @@
1
+ # @hasnaxyz/hook-checksecurity
2
+
3
+ Claude Code hook that runs security checks via Claude and Codex headless agents. This is a **blocker** hook on the Stop event.
4
+
5
+ ## Features
6
+
7
+ - **Stop event blocker**: Runs before session ends
8
+ - **Dual agent review**: Spawns both Claude and Codex for thorough coverage
9
+ - **Task dispatch**: Creates security tasks via `service-implementation task dispatch`
10
+ - **Repo pattern check**: Only runs for repos matching `[prefix]-[name]` pattern
11
+ - **One-time per session**: Only runs once (state flag prevents re-runs)
12
+ - **Session-aware**: Only runs for sessions matching configured keywords
13
+
14
+ ## Installation
15
+
16
+ ### Global CLI
17
+
18
+ ```bash
19
+ bun add -g @hasnaxyz/hook-checksecurity
20
+ hook-checksecurity install --global
21
+ ```
22
+
23
+ ### Project-specific
24
+
25
+ ```bash
26
+ cd /path/to/your/project
27
+ bunx @hasnaxyz/hook-checksecurity install
28
+ ```
29
+
30
+ ## Requirements
31
+
32
+ - `claude` CLI (for headless agent)
33
+ - `codex` CLI (optional, for additional security review)
34
+ - `service-implementation` CLI (for task dispatch)
35
+
36
+ ## Usage
37
+
38
+ Once installed, the hook runs automatically on the Stop event.
39
+
40
+ ### Commands
41
+
42
+ ```bash
43
+ hook-checksecurity install [path] # Install the hook
44
+ hook-checksecurity config [path] # Update configuration
45
+ hook-checksecurity uninstall [path] # Remove the hook
46
+ hook-checksecurity status # Show hook status
47
+ hook-checksecurity run # Execute hook (called by Claude Code)
48
+ ```
49
+
50
+ ### Options
51
+
52
+ - `--global`, `-g`: Apply to global settings (`~/.claude/settings.json`)
53
+ - `/path/to/repo`: Apply to specific project path
54
+
55
+ ## Configuration
56
+
57
+ Configuration is stored in `.claude/settings.json`:
58
+
59
+ ```json
60
+ {
61
+ "hooks": {
62
+ "Stop": [{
63
+ "hooks": [
64
+ {
65
+ "type": "command",
66
+ "command": "bunx @hasnaxyz/hook-checksecurity@latest run",
67
+ "timeout": 300
68
+ },
69
+ {
70
+ "type": "command",
71
+ "command": "bunx @hasnaxyz/hook-checktasks@latest run"
72
+ }
73
+ ]
74
+ }]
75
+ },
76
+ "checkSecurityConfig": {
77
+ "taskListId": "myproject-dev",
78
+ "keywords": ["dev"],
79
+ "enabled": true
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Options
85
+
86
+ | Option | Type | Default | Description |
87
+ |--------|------|---------|-------------|
88
+ | `taskListId` | string | auto | Task list for dispatching security tasks |
89
+ | `keywords` | string[] | ["dev"] | Only run for matching sessions |
90
+ | `enabled` | boolean | true | Enable/disable the hook |
91
+
92
+ ## How It Works
93
+
94
+ 1. **Stop event triggers**: When user tries to end session
95
+ 2. **Validates repo pattern**: Only runs for `[prefix]-[name]` folders
96
+ 3. **Checks session state**: Skips if already ran this session
97
+ 4. **Runs Claude security check**: Headless agent scans for vulnerabilities
98
+ 5. **Runs Codex security check**: Additional headless agent review
99
+ 6. **Creates tasks**: Both agents dispatch tasks via `service-implementation`
100
+ 7. **hook-checktasks blocks**: If tasks exist, session is blocked
101
+
102
+ ## Security Issues Detected
103
+
104
+ The hook checks for:
105
+
106
+ - Injection vulnerabilities (SQL, XSS, command injection)
107
+ - Authentication/authorization issues
108
+ - Sensitive data exposure
109
+ - Insecure configurations
110
+ - Dependency vulnerabilities
111
+ - Hardcoded secrets or credentials
112
+ - Input validation issues
113
+ - CSRF vulnerabilities
114
+ - Insecure deserialization
115
+
116
+ ## Task Format
117
+
118
+ Tasks are dispatched with:
119
+
120
+ ```bash
121
+ service-implementation task dispatch "myproject-dev" \
122
+ -s "SECURITY: [severity] - [brief description]" \
123
+ -d "[detailed description with file:line reference and remediation]"
124
+ ```
125
+
126
+ Severity levels: CRITICAL, HIGH, MEDIUM, LOW
127
+
128
+ ## Session State
129
+
130
+ State is persisted in `~/.claude/hook-state/checksecurity-{session_id}.json`:
131
+
132
+ ```json
133
+ {
134
+ "securityChecked": true,
135
+ "lastCheckRun": 1706500000000
136
+ }
137
+ ```
138
+
139
+ ## Hook Ordering
140
+
141
+ For proper operation, checksecurity should run **before** checktasks:
142
+
143
+ ```json
144
+ {
145
+ "hooks": {
146
+ "Stop": [{
147
+ "hooks": [
148
+ { "command": "bunx @hasnaxyz/hook-checksecurity@latest run" },
149
+ { "command": "bunx @hasnaxyz/hook-checktasks@latest run" }
150
+ ]
151
+ }]
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@hasnaxyz/hook-checksecurity",
3
+ "version": "0.1.4",
4
+ "description": "Claude Code hook that runs security checks via Claude and Codex headless agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-checksecurity": "./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
+ "codex",
32
+ "hook",
33
+ "security",
34
+ "headless",
35
+ "automation",
36
+ "cli"
37
+ ],
38
+ "author": "Hasna",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/hasnaxyz/hook-checksecurity.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
+ }