mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: Windows compatibility for hook scripts (execFileSync + tmux) (#215)
* fix: Windows compatibility for hook scripts
- post-edit-format.js: add `shell: process.platform === 'win32'` to
execFileSync options so npx.cmd is resolved via cmd.exe on Windows
- post-edit-typecheck.js: same fix for tsc invocation via npx
- hooks.json: skip tmux-dependent hooks on Windows where tmux is
unavailable (dev-server blocker and long-running command reminder)
On Windows, execFileSync('npx', ...) without shell:true fails with
ENOENT because Node.js cannot directly execute .cmd files. These
hooks silently fail on all Windows installations.
The tmux hooks unconditionally block dev server commands (exit 2) or
warn about tmux on Windows where tmux is not available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: parse Claude Code JSONL transcript format correctly
The session-end hook expected user messages at entry.content, but
Claude Code's actual JSONL format nests them at entry.message.content.
This caused all session files to be blank templates (0 user messages
despite 136+ actual entries).
- Check entry.message?.content in addition to entry.content
- Extract tool_use blocks from assistant message.content arrays
Verified with Claude Code v2.1.41 JSONL transcripts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: ddungan <sckim@mococo.co.kr>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(/(npm run dev\\b|pnpm( run)? dev\\b|yarn dev\\b|bun run dev\\b)/.test(cmd)){console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');console.error('[Hook] Use: tmux new-session -d -s dev \\\"npm run dev\\\"');console.error('[Hook] Then: tmux attach -t dev');process.exit(2)}}catch{}console.log(d)})\""
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(process.platform!=='win32'&&/(npm run dev\\b|pnpm( run)? dev\\b|yarn dev\\b|bun run dev\\b)/.test(cmd)){console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');console.error('[Hook] Use: tmux new-session -d -s dev \\\"npm run dev\\\"');console.error('[Hook] Then: tmux attach -t dev');process.exit(2)}}catch{}console.log(d)})\""
|
||||
}
|
||||
],
|
||||
"description": "Block dev servers outside tmux - ensures you can access logs"
|
||||
@@ -17,7 +17,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(!process.env.TMUX&&/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\\b|docker\\b|pytest|vitest|playwright)/.test(cmd)){console.error('[Hook] Consider running in tmux for session persistence');console.error('[Hook] tmux new -s dev | tmux attach -t dev')}}catch{}console.log(d)})\""
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(process.platform!=='win32'&&!process.env.TMUX&&/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\\b|docker\\b|pytest|vitest|playwright)/.test(cmd)){console.error('[Hook] Consider running in tmux for session persistence');console.error('[Hook] tmux new -s dev | tmux attach -t dev')}}catch{}console.log(d)})\""
|
||||
}
|
||||
],
|
||||
"description": "Reminder to use tmux for long-running commands"
|
||||
|
||||
@@ -29,7 +29,8 @@ process.stdin.on('end', () => {
|
||||
try {
|
||||
execFileSync('npx', ['prettier', '--write', filePath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 15000
|
||||
timeout: 15000,
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
} catch {
|
||||
// Prettier not installed, file missing, or failed — non-blocking
|
||||
|
||||
@@ -9,60 +9,70 @@
|
||||
* and reports only errors related to the edited file.
|
||||
*/
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||
let data = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
process.stdin.on("data", (chunk) => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
const input = JSON.parse(data);
|
||||
const filePath = input.tool_input?.file_path;
|
||||
|
||||
if (filePath && /\.(ts|tsx)$/.test(filePath)) {
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
if (!fs.existsSync(resolvedPath)) { console.log(data); return; }
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
console.log(data);
|
||||
return;
|
||||
}
|
||||
// Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop)
|
||||
let dir = path.dirname(resolvedPath);
|
||||
const root = path.parse(dir).root;
|
||||
let depth = 0;
|
||||
|
||||
while (dir !== root && depth < 20) {
|
||||
if (fs.existsSync(path.join(dir, 'tsconfig.json'))) {
|
||||
if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
|
||||
break;
|
||||
}
|
||||
dir = path.dirname(dir);
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (fs.existsSync(path.join(dir, 'tsconfig.json'))) {
|
||||
if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
|
||||
try {
|
||||
execFileSync('npx', ['tsc', '--noEmit', '--pretty', 'false'], {
|
||||
execFileSync("npx", ["tsc", "--noEmit", "--pretty", "false"], {
|
||||
cwd: dir,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 30000
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
timeout: 30000,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
} catch (err) {
|
||||
// tsc exits non-zero when there are errors — filter to edited file
|
||||
const output = (err.stdout || '') + (err.stderr || '');
|
||||
const output = (err.stdout || "") + (err.stderr || "");
|
||||
const relevantLines = output
|
||||
.split('\n')
|
||||
.filter(line => line.includes(filePath) || line.includes(path.basename(filePath)))
|
||||
.split("\n")
|
||||
.filter(
|
||||
(line) =>
|
||||
line.includes(filePath) ||
|
||||
line.includes(path.basename(filePath)),
|
||||
)
|
||||
.slice(0, 10);
|
||||
|
||||
if (relevantLines.length > 0) {
|
||||
console.error('[Hook] TypeScript errors in ' + path.basename(filePath) + ':');
|
||||
relevantLines.forEach(line => console.error(line));
|
||||
console.error(
|
||||
"[Hook] TypeScript errors in " + path.basename(filePath) + ":",
|
||||
);
|
||||
relevantLines.forEach((line) => console.error(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,18 +45,20 @@ function extractSessionSummary(transcriptPath) {
|
||||
const entry = JSON.parse(line);
|
||||
|
||||
// Collect user messages (first 200 chars each)
|
||||
if (entry.type === 'user' || entry.role === 'user') {
|
||||
const text = typeof entry.content === 'string'
|
||||
? entry.content
|
||||
: Array.isArray(entry.content)
|
||||
? entry.content.map(c => (c && c.text) || '').join(' ')
|
||||
if (entry.type === 'user' || entry.role === 'user' || entry.message?.role === 'user') {
|
||||
// Support both direct content and nested message.content (Claude Code JSONL format)
|
||||
const rawContent = entry.message?.content ?? entry.content;
|
||||
const text = typeof rawContent === 'string'
|
||||
? rawContent
|
||||
: Array.isArray(rawContent)
|
||||
? rawContent.map(c => (c && c.text) || '').join(' ')
|
||||
: '';
|
||||
if (text.trim()) {
|
||||
userMessages.push(text.trim().slice(0, 200));
|
||||
}
|
||||
}
|
||||
|
||||
// Collect tool names and modified files
|
||||
// Collect tool names and modified files (direct tool_use entries)
|
||||
if (entry.type === 'tool_use' || entry.tool_name) {
|
||||
const toolName = entry.tool_name || entry.name || '';
|
||||
if (toolName) toolsUsed.add(toolName);
|
||||
@@ -66,6 +68,21 @@ function extractSessionSummary(transcriptPath) {
|
||||
filesModified.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tool uses from assistant message content blocks (Claude Code JSONL format)
|
||||
if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
|
||||
for (const block of entry.message.content) {
|
||||
if (block.type === 'tool_use') {
|
||||
const toolName = block.name || '';
|
||||
if (toolName) toolsUsed.add(toolName);
|
||||
|
||||
const filePath = block.input?.file_path || '';
|
||||
if (filePath && (toolName === 'Edit' || toolName === 'Write')) {
|
||||
filesModified.add(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
parseErrors++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user