-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathauto-stage.js
More file actions
103 lines (89 loc) · 2.76 KB
/
Copy pathauto-stage.js
File metadata and controls
103 lines (89 loc) · 2.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env node
/**
* Auto Stage - PostToolUse Hook for Edit|Write
* Automatically stages files after Claude Code modifies them.
* Logs to: ~/.claude/hooks-logs/
*
* Benefits:
* - `git status` shows exactly what Claude modified
* - Easy to review changes before committing
* - No manual staging needed
*
* Note: Relies on .gitignore to exclude sensitive files (.env, keys, etc.)
*
* Setup in .claude/settings.json:
* {
* "hooks": {
* "PostToolUse": [{
* "matcher": "Edit|Write",
* "hooks": [{ "type": "command", "command": "node /path/to/auto-stage.js" }]
* }]
* }
* }
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const LOG_DIR = path.join(process.env.HOME, '.claude', 'hooks-logs');
function log(data) {
try {
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
const file = path.join(LOG_DIR, `${new Date().toISOString().slice(0, 10)}.jsonl`);
fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), hook: 'auto-stage', ...data }) + '\n');
} catch {}
}
function isInGitRepo(filePath) {
try {
const dir = path.dirname(filePath);
execSync('git rev-parse --git-dir', { cwd: dir, stdio: 'pipe' });
return true;
} catch {
return false;
}
}
function stageFile(filePath) {
try {
const dir = path.dirname(filePath);
execSync(`git add "${filePath}"`, { cwd: dir, stdio: 'pipe' });
return { success: true };
} catch (e) {
return { success: false, error: e.message };
}
}
async function main() {
let input = '';
for await (const chunk of process.stdin) input += chunk;
try {
const data = JSON.parse(input);
const { tool_name, tool_input, session_id, cwd } = data;
if (!['Edit', 'Write'].includes(tool_name)) {
return console.log('{}');
}
const filePath = tool_input?.file_path;
if (!filePath) {
log({ level: 'SKIP', reason: 'no file_path', tool: tool_name, session_id });
return console.log('{}');
}
// Resolve to absolute path if relative
const absPath = path.isAbsolute(filePath) ? filePath : path.join(cwd || process.cwd(), filePath);
if (!isInGitRepo(absPath)) {
log({ level: 'SKIP', reason: 'not in git repo', file: absPath, session_id });
return console.log('{}');
}
const result = stageFile(absPath);
if (result.success) {
log({ level: 'STAGED', file: absPath, tool: tool_name, session_id });
} else {
log({ level: 'ERROR', file: absPath, error: result.error, session_id });
}
console.log('{}');
} catch (e) {
log({ level: 'ERROR', error: e.message });
console.log('{}');
}
}
if (require.main === module) {
main();
} else {
module.exports = { isInGitRepo, stageFile, log };
}