claws-code 0.8.0
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/.claude/commands/claws-auto.md +90 -0
- package/.claude/commands/claws-bin.md +28 -0
- package/.claude/commands/claws-cleanup.md +28 -0
- package/.claude/commands/claws-do.md +82 -0
- package/.claude/commands/claws-fix.md +40 -0
- package/.claude/commands/claws-goal.md +111 -0
- package/.claude/commands/claws-help.md +54 -0
- package/.claude/commands/claws-plan.md +103 -0
- package/.claude/commands/claws-report.md +29 -0
- package/.claude/commands/claws-status.md +37 -0
- package/.claude/commands/claws-update.md +32 -0
- package/.claude/commands/claws.md +64 -0
- package/.claude/rules/claws-default-behavior.md +76 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.local.json +19 -0
- package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
- package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
- package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
- package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
- package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
- package/CHANGELOG.md +1949 -0
- package/LICENSE +21 -0
- package/README.md +420 -0
- package/bin/cli.js +84 -0
- package/cli.js +223 -0
- package/docs/ARCHITECTURE.md +511 -0
- package/docs/event-protocol.md +588 -0
- package/docs/features.md +562 -0
- package/docs/guide.md +891 -0
- package/docs/index.html +716 -0
- package/docs/protocol.md +323 -0
- package/extension/.vscodeignore +15 -0
- package/extension/CHANGELOG.md +1906 -0
- package/extension/LICENSE +21 -0
- package/extension/README.md +137 -0
- package/extension/docs/features.md +424 -0
- package/extension/docs/protocol.md +197 -0
- package/extension/esbuild.mjs +25 -0
- package/extension/icon.png +0 -0
- package/extension/native/.metadata.json +10 -0
- package/extension/native/node-pty/LICENSE +69 -0
- package/extension/native/node-pty/README.md +165 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
- package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
- package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
- package/extension/native/node-pty/lib/index.js +52 -0
- package/extension/native/node-pty/lib/index.js.map +1 -0
- package/extension/native/node-pty/lib/interfaces.js +7 -0
- package/extension/native/node-pty/lib/interfaces.js.map +1 -0
- package/extension/native/node-pty/lib/shared/conout.js +11 -0
- package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
- package/extension/native/node-pty/lib/terminal.js +190 -0
- package/extension/native/node-pty/lib/terminal.js.map +1 -0
- package/extension/native/node-pty/lib/types.js +7 -0
- package/extension/native/node-pty/lib/types.js.map +1 -0
- package/extension/native/node-pty/lib/unixTerminal.js +346 -0
- package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/utils.js +39 -0
- package/extension/native/node-pty/lib/utils.js.map +1 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
- package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
- package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
- package/extension/native/node-pty/package.json +64 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
- package/extension/package-lock.json +605 -0
- package/extension/package.json +343 -0
- package/extension/scripts/bundle-native.mjs +104 -0
- package/extension/scripts/deploy-dev.mjs +60 -0
- package/extension/src/ansi-strip.ts +52 -0
- package/extension/src/backends/vscode/claws-pty.ts +483 -0
- package/extension/src/backends/vscode/status-bar.ts +99 -0
- package/extension/src/backends/vscode/vscode-backend.ts +282 -0
- package/extension/src/capture-store.ts +125 -0
- package/extension/src/event-log.ts +629 -0
- package/extension/src/event-schemas.ts +478 -0
- package/extension/src/extension.js +492 -0
- package/extension/src/extension.ts +873 -0
- package/extension/src/lifecycle-engine.ts +60 -0
- package/extension/src/lifecycle-rules.ts +171 -0
- package/extension/src/lifecycle-store.ts +506 -0
- package/extension/src/peer-registry.ts +176 -0
- package/extension/src/pipeline-registry.ts +82 -0
- package/extension/src/platform.ts +64 -0
- package/extension/src/protocol.ts +532 -0
- package/extension/src/server-config.ts +98 -0
- package/extension/src/server.ts +2210 -0
- package/extension/src/task-registry.ts +51 -0
- package/extension/src/terminal-backend.ts +211 -0
- package/extension/src/terminal-manager.ts +395 -0
- package/extension/src/topic-registry.ts +70 -0
- package/extension/src/topic-utils.ts +46 -0
- package/extension/src/transport.ts +45 -0
- package/extension/src/uninstall-cleanup.ts +232 -0
- package/extension/src/wave-registry.ts +314 -0
- package/extension/src/websocket-transport.ts +153 -0
- package/extension/tsconfig.json +23 -0
- package/lib/capabilities.js +145 -0
- package/lib/dry-run.js +43 -0
- package/lib/install.js +1018 -0
- package/lib/mcp-setup.js +92 -0
- package/lib/platform.js +240 -0
- package/lib/preflight.js +152 -0
- package/lib/shell-hook.js +343 -0
- package/lib/uninstall.js +162 -0
- package/lib/verify.js +166 -0
- package/mcp_server.js +3529 -0
- package/package.json +48 -0
- package/rules/claws-default-behavior.md +72 -0
- package/scripts/_helpers/atomic-file.mjs +137 -0
- package/scripts/_helpers/fix-repair.js +64 -0
- package/scripts/_helpers/json-safe.mjs +218 -0
- package/scripts/bump-version.sh +84 -0
- package/scripts/codegen/gen-docs.mjs +61 -0
- package/scripts/codegen/gen-json-schema.mjs +62 -0
- package/scripts/codegen/gen-mcp-tools.mjs +358 -0
- package/scripts/codegen/gen-types.mjs +172 -0
- package/scripts/codegen/index.mjs +42 -0
- package/scripts/dev-hooks/check-extension-dirs.js +77 -0
- package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
- package/scripts/dev-hooks/check-stale-main.js +55 -0
- package/scripts/dev-hooks/check-tag-pushed.js +51 -0
- package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
- package/scripts/dev-vsix-install.sh +60 -0
- package/scripts/fix.sh +702 -0
- package/scripts/gen-client-types.mjs +81 -0
- package/scripts/git-hooks/pre-commit +31 -0
- package/scripts/hooks/lifecycle-state.js +61 -0
- package/scripts/hooks/package.json +4 -0
- package/scripts/hooks/post-tool-use-claws.js +292 -0
- package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
- package/scripts/hooks/pre-tool-use-claws.js +206 -0
- package/scripts/hooks/session-start-claws.js +97 -0
- package/scripts/hooks/stop-claws.js +88 -0
- package/scripts/inject-claude-md.js +205 -0
- package/scripts/inject-dev-hooks.js +96 -0
- package/scripts/inject-global-claude-md.js +140 -0
- package/scripts/inject-settings-hooks.js +370 -0
- package/scripts/install.ps1 +146 -0
- package/scripts/install.sh +1729 -0
- package/scripts/monitor-arm-watch.js +155 -0
- package/scripts/rebuild-node-pty.sh +245 -0
- package/scripts/report.sh +232 -0
- package/scripts/shell-hook.fish +164 -0
- package/scripts/shell-hook.ps1 +33 -0
- package/scripts/shell-hook.sh +232 -0
- package/scripts/stream-events.js +399 -0
- package/scripts/terminal-wrapper.sh +36 -0
- package/scripts/test-enforcement.sh +132 -0
- package/scripts/test-install.sh +174 -0
- package/scripts/test-installer-parity.sh +135 -0
- package/scripts/test-template-enforcement.sh +76 -0
- package/scripts/uninstall.sh +143 -0
- package/scripts/update.sh +337 -0
- package/scripts/verify-release.sh +323 -0
- package/scripts/verify-wrapped.sh +194 -0
- package/templates/CLAUDE.global.md +135 -0
- package/templates/CLAUDE.project.md +37 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gen-client-types.mjs — Generate schemas/client-types.d.ts from SCHEMA_BY_NAME.
|
|
3
|
+
// Usage: node scripts/gen-client-types.mjs (from repo root)
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const repoRoot = join(__dirname, '..');
|
|
14
|
+
const extRoot = join(repoRoot, 'extension');
|
|
15
|
+
const bundlePath = join(extRoot, 'dist', 'event-schemas.bundle.cjs');
|
|
16
|
+
|
|
17
|
+
mkdirSync(join(extRoot, 'dist'), { recursive: true });
|
|
18
|
+
|
|
19
|
+
const esbuildBin = join(extRoot, 'node_modules', '.bin', 'esbuild');
|
|
20
|
+
const srcPath = join(extRoot, 'src', 'event-schemas.ts');
|
|
21
|
+
execSync(
|
|
22
|
+
`"${esbuildBin}" "${srcPath}" --bundle --format=cjs --platform=node --outfile="${bundlePath}"`,
|
|
23
|
+
{ stdio: 'pipe' },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const require = createRequire(__filename);
|
|
27
|
+
const mod = require(bundlePath);
|
|
28
|
+
const SCHEMA_BY_NAME = mod.SCHEMA_BY_NAME ?? {};
|
|
29
|
+
|
|
30
|
+
function zodToTs(schema, depth = 0) {
|
|
31
|
+
const def = schema._def;
|
|
32
|
+
const t = def.typeName ?? 'unknown';
|
|
33
|
+
const pad = ' '.repeat(depth);
|
|
34
|
+
|
|
35
|
+
switch (t) {
|
|
36
|
+
case 'ZodObject': {
|
|
37
|
+
const shape = schema.shape;
|
|
38
|
+
const fields = Object.entries(shape).map(([k, v]) => {
|
|
39
|
+
const isOpt = v._def.typeName === 'ZodOptional';
|
|
40
|
+
const inner = isOpt ? v._def.innerType : v;
|
|
41
|
+
return `${pad} ${k}${isOpt ? '?' : ''}: ${zodToTs(inner, depth + 1)};`;
|
|
42
|
+
}).join('\n');
|
|
43
|
+
return `{\n${fields}\n${pad}}`;
|
|
44
|
+
}
|
|
45
|
+
case 'ZodString': return 'string';
|
|
46
|
+
case 'ZodNumber': return 'number';
|
|
47
|
+
case 'ZodBoolean': return 'boolean';
|
|
48
|
+
case 'ZodUnknown': return 'unknown';
|
|
49
|
+
case 'ZodNull': return 'null';
|
|
50
|
+
case 'ZodLiteral': return JSON.stringify(def.value);
|
|
51
|
+
case 'ZodEnum': return def.values.map(v => JSON.stringify(v)).join(' | ');
|
|
52
|
+
case 'ZodArray': return `Array<${zodToTs(def.type, depth)}>`;
|
|
53
|
+
case 'ZodRecord': return `Record<string, ${zodToTs(def.valueType, depth)}>`;
|
|
54
|
+
case 'ZodNullable': return `${zodToTs(def.innerType, depth)} | null`;
|
|
55
|
+
case 'ZodOptional': return zodToTs(def.innerType, depth);
|
|
56
|
+
case 'ZodUnion': return def.options.map(o => zodToTs(o, depth)).join(' | ');
|
|
57
|
+
default: return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toPascal(kebab) {
|
|
62
|
+
return kebab
|
|
63
|
+
.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())
|
|
64
|
+
.replace(/^[a-z]/, c => c.toUpperCase());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = [
|
|
68
|
+
'// @generated — do not edit. Run: node scripts/gen-client-types.mjs',
|
|
69
|
+
'// Source: extension/src/event-schemas.ts — SCHEMA_BY_NAME',
|
|
70
|
+
'',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const [name, schema] of Object.entries(SCHEMA_BY_NAME)) {
|
|
74
|
+
const iface = toPascal(name);
|
|
75
|
+
lines.push(`export interface ${iface} ${zodToTs(schema)}`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const outPath = join(repoRoot, 'schemas', 'client-types.d.ts');
|
|
80
|
+
writeFileSync(outPath, lines.join('\n'), 'utf8');
|
|
81
|
+
console.log(`[gen-client-types] wrote schemas/client-types.d.ts (${Object.keys(SCHEMA_BY_NAME).length} types)`);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claws pre-commit hook — enforces CHANGELOG.md is updated for code changes.
|
|
3
|
+
# Installed by scripts/install.sh into .git/hooks/pre-commit.
|
|
4
|
+
|
|
5
|
+
staged=$(git diff --cached --name-only 2>/dev/null)
|
|
6
|
+
|
|
7
|
+
# Only care about commits that touch code files
|
|
8
|
+
code_files=$(echo "$staged" | grep -E '\.(ts|js|mjs|sh)$' | grep -v '\.d\.ts$' || true)
|
|
9
|
+
|
|
10
|
+
if [ -z "$code_files" ]; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# CHANGELOG.md must also be staged
|
|
15
|
+
if echo "$staged" | grep -q "^CHANGELOG\.md$"; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo ""
|
|
20
|
+
echo " pre-commit: CHANGELOG.md not staged"
|
|
21
|
+
echo ""
|
|
22
|
+
echo " Code files in this commit:"
|
|
23
|
+
echo "$code_files" | sed 's/^/ /'
|
|
24
|
+
echo ""
|
|
25
|
+
echo " Update CHANGELOG.md with what changed, then:"
|
|
26
|
+
echo " git add CHANGELOG.md && git commit"
|
|
27
|
+
echo ""
|
|
28
|
+
echo " To bypass (chores, docs-only, WIP):"
|
|
29
|
+
echo " git commit --no-verify"
|
|
30
|
+
echo ""
|
|
31
|
+
exit 1
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Shared module — read/write .claws/lifecycle-state.json
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const PHASES = [
|
|
8
|
+
'PLAN-REQUIRED',
|
|
9
|
+
'PLAN',
|
|
10
|
+
'SPAWN',
|
|
11
|
+
'DEPLOY',
|
|
12
|
+
'OBSERVE',
|
|
13
|
+
'RECOVER',
|
|
14
|
+
'HARVEST',
|
|
15
|
+
'CLEANUP',
|
|
16
|
+
'REFLECT',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function phaseIndex(phase) {
|
|
20
|
+
return PHASES.indexOf(phase);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function statePath(cwd) {
|
|
24
|
+
return path.join(cwd, '.claws', 'lifecycle-state.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readState(cwd) {
|
|
28
|
+
const p = statePath(cwd);
|
|
29
|
+
if (!fs.existsSync(p)) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeState(cwd, state) {
|
|
38
|
+
const p = statePath(cwd);
|
|
39
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
40
|
+
// M-29: atomic write (tmp + renameSync) — mirrors extension/src/lifecycle-store.ts.
|
|
41
|
+
// Prevents partial lifecycle-state.json if the hook process is killed mid-write.
|
|
42
|
+
// F3: open+writeSync+fsyncSync+close before rename for durability on power-cut.
|
|
43
|
+
const content = JSON.stringify(state, null, 2) + '\n';
|
|
44
|
+
const tmp = p + '.claws-tmp.' + process.pid + '-' + (++writeState._nonce);
|
|
45
|
+
let _fd;
|
|
46
|
+
try {
|
|
47
|
+
_fd = fs.openSync(tmp, 'w', 0o644);
|
|
48
|
+
fs.writeSync(_fd, content);
|
|
49
|
+
fs.fsyncSync(_fd);
|
|
50
|
+
fs.closeSync(_fd);
|
|
51
|
+
_fd = null;
|
|
52
|
+
fs.renameSync(tmp, p);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (_fd != null) { try { fs.closeSync(_fd); } catch { /* ignore */ } }
|
|
55
|
+
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
writeState._nonce = 0;
|
|
60
|
+
|
|
61
|
+
module.exports = { readState, writeState, PHASES, phaseIndex };
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try { require('fs').appendFileSync('/tmp/claws-hook-trace.log', `${new Date().toISOString()} hook-fired pid=${process.pid} cwd=${process.cwd()} argv=${JSON.stringify(process.argv)}\n`); } catch {}
|
|
3
|
+
// Claws PostToolUse hook — fail-closes the spawn → monitor race window.
|
|
4
|
+
//
|
|
5
|
+
// Fires after every MCP tool call. Only acts on spawn-class tools
|
|
6
|
+
// (claws_create/worker/fleet/dispatch_subworker) where the call succeeded
|
|
7
|
+
// and returned a terminal_id. Waits up to ~4s for lifecycle.monitors to
|
|
8
|
+
// register the terminal (4s nominal so cleanup has ~1s before the 5s self-kill).
|
|
9
|
+
// If missing, publishes wave.violation event + auto-closes the orphaned terminal.
|
|
10
|
+
//
|
|
11
|
+
// SAFETY CONTRACT (P5): never crash, never block, never exit non-zero.
|
|
12
|
+
// Errors silently swallowed unless CLAWS_DEBUG=1.
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// M-24: gate error handlers on CLAWS_DEBUG — when CLAWS_DEBUG=1, errors
|
|
16
|
+
// propagate visibly for debugging instead of being silently swallowed.
|
|
17
|
+
if (!process.env.CLAWS_DEBUG) {
|
|
18
|
+
process.on('uncaughtException', () => { try { process.exit(0); } catch {} });
|
|
19
|
+
process.on('unhandledRejection', () => { try { process.exit(0); } catch {} });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// M-13: 5-second self-kill safety timer — hook can never hang the parent process.
|
|
24
|
+
setTimeout(() => { process.exit(0); }, 5000).unref();
|
|
25
|
+
|
|
26
|
+
const SPAWN_CLASS = new Set([
|
|
27
|
+
'mcp__claws__claws_create',
|
|
28
|
+
'mcp__claws__claws_worker',
|
|
29
|
+
'mcp__claws__claws_fleet',
|
|
30
|
+
'mcp__claws__claws_dispatch_subworker',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Monitor wait window: 4000 ms (< 5 s self-kill) leaves ~1 s for violation
|
|
34
|
+
// cleanup (publish + close) before the self-kill fires.
|
|
35
|
+
const MONITOR_WAIT_MS = 4000;
|
|
36
|
+
const MONITOR_POLL_MS = 500;
|
|
37
|
+
|
|
38
|
+
let input = '';
|
|
39
|
+
// M-13: single try block for both 'data' and 'end' — fail together or not at all.
|
|
40
|
+
try {
|
|
41
|
+
process.stdin.on('data', d => { input += d; });
|
|
42
|
+
process.stdin.on('end', () => {
|
|
43
|
+
run(input).catch(() => { try { process.exit(0); } catch {} });
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Append a structured diagnostic line to /tmp/claws-hook-diag.log (never throws).
|
|
50
|
+
function writeDiag(event, detail) {
|
|
51
|
+
try {
|
|
52
|
+
const line = `${new Date().toISOString()} hook-diag ${event} ${JSON.stringify(detail)}\n`;
|
|
53
|
+
require('fs').appendFileSync('/tmp/claws-hook-diag.log', line);
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// BUG6-L1 (v0714, .local/plans/v0714/investigations/bug6-hook-nested-context.md):
|
|
58
|
+
// Forward-compat normalizer. Handles all observed Claude Code tool_response shapes and
|
|
59
|
+
// returns null for unknown/unparseable input so callers can log a diagnostic instead of
|
|
60
|
+
// silently proceeding with a broken value.
|
|
61
|
+
//
|
|
62
|
+
// Shapes handled:
|
|
63
|
+
// 1. Bare array: [{type:'text', text:'<JSON>'}] ← current Claude Code
|
|
64
|
+
// 2. Wrapped object: {content:[{type:'text', text:'<JSON>'}]} ← older Claude Code
|
|
65
|
+
// 3. Plain object: {ok:true, terminal_id:..., ...} ← already-unwrapped / tests
|
|
66
|
+
// 4. null / undefined / primitive → null
|
|
67
|
+
// 5. Unknown shape → null + diagnostic
|
|
68
|
+
function unwrapMcpResponse(resp) {
|
|
69
|
+
if (resp == null || typeof resp !== 'object') {
|
|
70
|
+
writeDiag('unwrap-null-or-primitive', { type: typeof resp });
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Shape 3 — plain object already unwrapped
|
|
75
|
+
if (resp.ok !== undefined) return resp;
|
|
76
|
+
|
|
77
|
+
// Shape 1 — bare array of content blocks (current Claude Code)
|
|
78
|
+
if (Array.isArray(resp) && resp[0] && typeof resp[0].text === 'string') {
|
|
79
|
+
try { return JSON.parse(resp[0].text); }
|
|
80
|
+
catch (e) { writeDiag('unwrap-bare-array-parse-fail', { error: e.message, preview: resp[0].text.slice(0, 200) }); return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Shape 2 — wrapped object with content array (older Claude Code)
|
|
84
|
+
if (Array.isArray(resp.content) && resp.content[0] && typeof resp.content[0].text === 'string') {
|
|
85
|
+
try { return JSON.parse(resp.content[0].text); }
|
|
86
|
+
catch (e) { writeDiag('unwrap-wrapped-parse-fail', { error: e.message, preview: resp.content[0].text.slice(0, 200) }); return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unknown shape — emit diagnostic so future format changes are observable
|
|
90
|
+
writeDiag('unwrap-unknown-shape', { keys: Object.keys(resp), isArray: Array.isArray(resp), preview: JSON.stringify(resp).slice(0, 300) });
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function run(raw) {
|
|
95
|
+
try { require('fs').appendFileSync(`/tmp/claws-hook-stdin-${process.pid}.json`, raw); } catch {}
|
|
96
|
+
try {
|
|
97
|
+
const fs = require('fs');
|
|
98
|
+
const path = require('path');
|
|
99
|
+
|
|
100
|
+
let data = {};
|
|
101
|
+
try { data = JSON.parse(raw); } catch { process.exit(0); return; }
|
|
102
|
+
|
|
103
|
+
const toolName = data.tool_name || '';
|
|
104
|
+
if (!SPAWN_CLASS.has(toolName)) { process.exit(0); return; }
|
|
105
|
+
|
|
106
|
+
const resp = unwrapMcpResponse(data.tool_response);
|
|
107
|
+
if (!resp) { writeDiag('unwrap-failed', { tool: toolName }); process.exit(0); return; }
|
|
108
|
+
if (!resp.ok) { process.exit(0); return; }
|
|
109
|
+
|
|
110
|
+
const terminalIds = extractTerminalIds(resp);
|
|
111
|
+
if (terminalIds.length === 0) { process.exit(0); return; }
|
|
112
|
+
|
|
113
|
+
const cwd = data.cwd || process.cwd();
|
|
114
|
+
const socketPath = findSocket(cwd);
|
|
115
|
+
if (!socketPath) { process.exit(0); return; }
|
|
116
|
+
|
|
117
|
+
// Layer 1: spawn per-worker pgrep watchers (fire-and-forget, <1ms).
|
|
118
|
+
// Complements the lifecycle.snapshot check below — verifies an OS-level
|
|
119
|
+
// stream-events.js process is actually alive watching the corrId, not just
|
|
120
|
+
// that lifecycle.monitors[] was pre-populated by mcp_server at spawn time.
|
|
121
|
+
spawnWatchers(extractWorkerEntries(resp), socketPath);
|
|
122
|
+
|
|
123
|
+
for (const tid of terminalIds) {
|
|
124
|
+
await checkOne(socketPath, tid, toolName);
|
|
125
|
+
}
|
|
126
|
+
process.exit(0);
|
|
127
|
+
} catch {
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Returns [{terminalId, corrId}] for spawning per-worker pgrep watchers.
|
|
133
|
+
function extractWorkerEntries(resp) {
|
|
134
|
+
if (Array.isArray(resp.workers)) {
|
|
135
|
+
return resp.workers
|
|
136
|
+
.filter(w => w && (w.terminal_id != null || w.id != null))
|
|
137
|
+
.map(w => ({
|
|
138
|
+
terminalId: w.terminal_id != null ? w.terminal_id : w.id,
|
|
139
|
+
corrId: w.correlation_id || null,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
const tid = resp.terminal_id != null ? resp.terminal_id
|
|
143
|
+
: Array.isArray(resp.terminal_ids) && resp.terminal_ids[0] != null ? resp.terminal_ids[0]
|
|
144
|
+
: resp.id != null ? resp.id
|
|
145
|
+
: null;
|
|
146
|
+
return [{ terminalId: tid, corrId: resp.correlation_id || null }];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function spawnWatchers(entries, socketPath) {
|
|
150
|
+
const { spawn } = require('child_process');
|
|
151
|
+
const path = require('path');
|
|
152
|
+
const fs = require('fs');
|
|
153
|
+
const watcherPath = path.join(__dirname, '..', 'monitor-arm-watch.js');
|
|
154
|
+
// Derive the project root from the socket path so the watcher's cwd is
|
|
155
|
+
// deterministic regardless of where the hook process was launched from.
|
|
156
|
+
const projectRoot = socketPath
|
|
157
|
+
? path.dirname(path.dirname(socketPath))
|
|
158
|
+
: undefined;
|
|
159
|
+
for (const { terminalId, corrId } of entries) {
|
|
160
|
+
if (!corrId || terminalId == null) continue;
|
|
161
|
+
try {
|
|
162
|
+
const debugLog = `/tmp/claws-monitor-arm-watch-${corrId}.log`;
|
|
163
|
+
const fd = fs.openSync(debugLog, 'a');
|
|
164
|
+
spawn(process.execPath, [
|
|
165
|
+
watcherPath,
|
|
166
|
+
'--corr-id', corrId,
|
|
167
|
+
'--term-id', String(terminalId),
|
|
168
|
+
'--grace-ms', '10000',
|
|
169
|
+
'--socket', socketPath,
|
|
170
|
+
], {
|
|
171
|
+
detached: true,
|
|
172
|
+
stdio: ['ignore', fd, fd],
|
|
173
|
+
cwd: projectRoot,
|
|
174
|
+
}).unref();
|
|
175
|
+
try { fs.closeSync(fd); } catch {}
|
|
176
|
+
} catch { /* fire-and-forget — never throw */ }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function extractTerminalIds(resp) {
|
|
181
|
+
if (Array.isArray(resp.terminal_ids)) {
|
|
182
|
+
return resp.terminal_ids.filter(id => id != null);
|
|
183
|
+
}
|
|
184
|
+
if (resp.terminal_id != null) return [resp.terminal_id];
|
|
185
|
+
// claws_create returns { id, logPath? } — use id as fallback
|
|
186
|
+
if (resp.id != null) return [resp.id];
|
|
187
|
+
// claws_fleet: { workers: [{terminal_id, ...}] }
|
|
188
|
+
if (Array.isArray(resp.workers)) {
|
|
189
|
+
return resp.workers
|
|
190
|
+
.filter(w => w && (w.terminal_id != null || w.id != null))
|
|
191
|
+
.map(w => (w.terminal_id != null ? w.terminal_id : w.id));
|
|
192
|
+
}
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function checkOne(socketPath, terminalId, toolName) {
|
|
197
|
+
const registered = await waitForMonitor(socketPath, terminalId, MONITOR_WAIT_MS, MONITOR_POLL_MS);
|
|
198
|
+
if (registered) return;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
process.stderr.write(
|
|
202
|
+
`[claws] PostToolUse: monitor not registered for terminal ${terminalId} within 5s.` +
|
|
203
|
+
` Auto-cancelling spawn (use Monitor + scripts/stream-events.js | grep pattern next time).\n`
|
|
204
|
+
);
|
|
205
|
+
} catch {}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await sendCmd(socketPath, {
|
|
209
|
+
cmd: 'publish',
|
|
210
|
+
topic: 'wave.violation',
|
|
211
|
+
payload: { kind: 'monitor-missing', terminal_id: terminalId, tool_name: toolName, ts: new Date().toISOString() },
|
|
212
|
+
});
|
|
213
|
+
} catch {}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await sendCmd(socketPath, { cmd: 'close', id: terminalId });
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function waitForMonitor(socketPath, terminalId, maxWaitMs, intervalMs) {
|
|
221
|
+
const deadline = Date.now() + maxWaitMs;
|
|
222
|
+
while (Date.now() < deadline) {
|
|
223
|
+
try {
|
|
224
|
+
const snap = await sendCmd(socketPath, { cmd: 'lifecycle.snapshot' });
|
|
225
|
+
if (monitorPresent(snap, terminalId)) return true;
|
|
226
|
+
} catch { /* socket error — retry */ }
|
|
227
|
+
const remaining = deadline - Date.now();
|
|
228
|
+
if (remaining <= 0) break;
|
|
229
|
+
await sleep(Math.min(intervalMs, remaining));
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function monitorPresent(snap, terminalId) {
|
|
235
|
+
if (!snap || !snap.ok) return false;
|
|
236
|
+
const state = snap.state || snap;
|
|
237
|
+
const monitors = state.monitors;
|
|
238
|
+
if (!monitors) return false;
|
|
239
|
+
if (Array.isArray(monitors)) {
|
|
240
|
+
return monitors.some(m => m && String(m.terminal_id) === String(terminalId));
|
|
241
|
+
}
|
|
242
|
+
return Object.prototype.hasOwnProperty.call(monitors, String(terminalId));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function sendCmd(socketPath, obj) {
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
const net = require('net');
|
|
248
|
+
const id = Math.random().toString(36).slice(2);
|
|
249
|
+
const msg = JSON.stringify({ id, ...obj }) + '\n';
|
|
250
|
+
let buf = '';
|
|
251
|
+
let done = false;
|
|
252
|
+
const sock = net.createConnection(socketPath);
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
if (!done) { done = true; try { sock.destroy(); } catch {} reject(new Error('timeout')); }
|
|
255
|
+
}, 2000);
|
|
256
|
+
sock.on('connect', () => {
|
|
257
|
+
try { sock.write(msg); } catch (e) {
|
|
258
|
+
if (!done) { done = true; clearTimeout(timer); reject(e); }
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
sock.on('data', chunk => {
|
|
262
|
+
buf += chunk;
|
|
263
|
+
const nl = buf.indexOf('\n');
|
|
264
|
+
if (nl !== -1 && !done) {
|
|
265
|
+
done = true;
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
try { sock.destroy(); } catch {}
|
|
268
|
+
try { resolve(JSON.parse(buf.slice(0, nl))); } catch (e) { reject(e); }
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
sock.on('error', e => { if (!done) { done = true; clearTimeout(timer); reject(e); } });
|
|
272
|
+
sock.on('close', () => { if (!done) { done = true; clearTimeout(timer); reject(new Error('closed')); } });
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function findSocket(startDir) {
|
|
277
|
+
const path = require('path');
|
|
278
|
+
const fs = require('fs');
|
|
279
|
+
let dir = startDir;
|
|
280
|
+
for (let i = 0; i < 10; i++) {
|
|
281
|
+
const candidate = path.join(dir, '.claws', 'claws.sock');
|
|
282
|
+
try { if (fs.existsSync(candidate)) return candidate; } catch {}
|
|
283
|
+
const parent = path.dirname(dir);
|
|
284
|
+
if (parent === dir) break;
|
|
285
|
+
dir = parent;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function sleep(ms) {
|
|
291
|
+
return new Promise(r => setTimeout(r, ms));
|
|
292
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook: blocks `git commit --no-verify` (and similar bypass flags)
|
|
3
|
+
// per advisory-mechanism-audit Finding F32.
|
|
4
|
+
//
|
|
5
|
+
// Workers/orchestrators that try to skip pre-commit hooks get a hard
|
|
6
|
+
// rejection with a clear error message.
|
|
7
|
+
//
|
|
8
|
+
// Hook input: Claude Code passes JSON via stdin: { tool: 'Bash', args: { command: '...' } }
|
|
9
|
+
// Hook output: exit 0 = allow; exit 1 with stderr message = block.
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
if (!process.env.CLAWS_DEBUG) {
|
|
14
|
+
process.on('uncaughtException', () => { try { process.exit(0); } catch {} });
|
|
15
|
+
process.on('unhandledRejection', () => { try { process.exit(0); } catch {} });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const LOG = '/tmp/claws-hook-no-verify.log';
|
|
20
|
+
|
|
21
|
+
function log(msg) {
|
|
22
|
+
try {
|
|
23
|
+
fs.appendFileSync(LOG, `${new Date().toISOString()} ${msg}\n`);
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Self-kill safety — hook can never hang the parent process.
|
|
28
|
+
setTimeout(() => { log('hook self-kill 5s'); process.exit(0); }, 5000).unref();
|
|
29
|
+
|
|
30
|
+
let input = '';
|
|
31
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
32
|
+
process.stdin.on('end', () => {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(input || '{}');
|
|
36
|
+
} catch (e) {
|
|
37
|
+
log(`malformed input: ${e.message}`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cmd = (parsed.tool_input && parsed.tool_input.command) ||
|
|
42
|
+
(parsed.args && parsed.args.command) || '';
|
|
43
|
+
if (typeof cmd !== 'string') process.exit(0);
|
|
44
|
+
|
|
45
|
+
// Match git commit/rebase/push with bypass flags.
|
|
46
|
+
// The -n short form regex uses a negative lookahead to avoid matching -name, -no, etc.
|
|
47
|
+
const blockedPatterns = [
|
|
48
|
+
/\bgit\s+commit\b[^|;&\n]*--no-verify\b/,
|
|
49
|
+
/\bgit\s+commit\b[^|;&\n]*\s-n(?![a-zA-Z0-9_-])/,
|
|
50
|
+
/\bgit\s+commit\b[^|;&\n]*--no-gpg-sign\b/,
|
|
51
|
+
/\bgit\s+rebase\b[^|;&\n]*--no-verify\b/,
|
|
52
|
+
/\bgit\s+push\b[^|;&\n]*--no-verify\b/,
|
|
53
|
+
/\bcommit\.gpgsign=false\b/,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const re of blockedPatterns) {
|
|
57
|
+
if (re.test(cmd)) {
|
|
58
|
+
const reason =
|
|
59
|
+
'[claws-hook] BLOCKED: bypass flag detected in git command. ' +
|
|
60
|
+
'Pre-commit hooks exist for a reason (tests, lint, type checks). ' +
|
|
61
|
+
'Fix the underlying issue instead of bypassing. ' +
|
|
62
|
+
'If you genuinely need to bypass, ask the human user explicitly. ' +
|
|
63
|
+
`Pattern matched: ${re.source}`;
|
|
64
|
+
log(`BLOCKED: ${cmd.slice(0, 200)}`);
|
|
65
|
+
process.stderr.write(reason + '\n');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
log(`ALLOWED: ${cmd.slice(0, 100)}`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
});
|