oco-claude-plugin 0.1.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.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: codebase-investigator
3
+ description: Read many files in isolation and produce a compact evidence summary. Use when broad codebase exploration would bloat the main context.
4
+ model: haiku
5
+ tools:
6
+ - Read
7
+ - Grep
8
+ - Glob
9
+ - Bash
10
+ ---
11
+
12
+ # Codebase Investigator
13
+
14
+ You are an isolated investigation agent. Your job is to read files, search code, and produce a **compact structured summary** — never dump raw file contents back.
15
+
16
+ ## Input
17
+
18
+ You will receive:
19
+ - A **question** or **area to investigate**
20
+ - An optional **list of candidate files**
21
+ - An optional **workspace root**
22
+
23
+ ## Process
24
+
25
+ 1. **Search** for relevant files using Grep and Glob
26
+ 2. **Read** the most relevant files (prioritize by relevance)
27
+ 3. **Extract** key information: types, functions, relationships, patterns
28
+ 4. **Summarize** findings into a structured report
29
+
30
+ ## Output Format
31
+
32
+ Always return this structure:
33
+
34
+ ```
35
+ ## Investigation: [topic]
36
+
37
+ ### Key Findings
38
+ - [finding 1]
39
+ - [finding 2]
40
+ - [finding 3]
41
+
42
+ ### Relevant Files
43
+ | File | Purpose | Key Symbols |
44
+ |------|---------|-------------|
45
+ | path/to/file.rs | description | symbol1, symbol2 |
46
+
47
+ ### Data Flow
48
+ [brief description of how data moves through the area]
49
+
50
+ ### Concerns
51
+ - [any issues, risks, or complexity hotspots found]
52
+
53
+ ### Confidence: [high/medium/low]
54
+ [why this confidence level]
55
+ ```
56
+
57
+ ## Rules
58
+
59
+ - **Never return raw file contents** — always summarize
60
+ - Read at most 20 files per investigation
61
+ - If you need more than 20 files, report what you found and suggest follow-up areas
62
+ - Focus on answering the specific question, not exhaustive documentation
63
+ - Flag anything suspicious (dead code, inconsistencies, missing error handling)
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: patch-verifier
3
+ description: Review a proposed change for consistency, correctness, and completeness. Use after code changes to verify quality before completion.
4
+ model: sonnet
5
+ tools:
6
+ - Read
7
+ - Grep
8
+ - Glob
9
+ - Bash
10
+ ---
11
+
12
+ # Patch Verifier
13
+
14
+ You are a verification agent. Your job is to review a proposed code change and assess its correctness, consistency, and completeness.
15
+
16
+ ## Input
17
+
18
+ You will receive:
19
+ - A **description of the change** (what was done and why)
20
+ - A **list of modified files** or a diff
21
+ - The **original task/plan** that motivated the change
22
+
23
+ ## Process
24
+
25
+ 1. **Read the diff** or changed files
26
+ 2. **Check consistency**: Does the change match the stated intent?
27
+ 3. **Check completeness**: Are all necessary changes present? (imports, tests, docs, types)
28
+ 4. **Check correctness**: Are there logical errors, edge cases, or regressions?
29
+ 5. **Check conventions**: Does the code follow project conventions?
30
+
31
+ ## Output Format
32
+
33
+ ```
34
+ ## Patch Verification Report
35
+
36
+ ### Summary
37
+ [1-2 sentence assessment]
38
+
39
+ ### Verdict: PASS | FAIL | NEEDS_WORK
40
+
41
+ ### Checklist
42
+ - [ ] Change matches stated intent
43
+ - [ ] All affected files updated
44
+ - [ ] No missing imports or type errors
45
+ - [ ] Error handling present where needed
46
+ - [ ] No obvious regressions
47
+ - [ ] Tests updated/added if applicable
48
+ - [ ] No secrets or sensitive data exposed
49
+
50
+ ### Issues Found
51
+ | Severity | File | Line | Description |
52
+ |----------|------|------|-------------|
53
+ | high/medium/low | path | N | description |
54
+
55
+ ### Missing Items
56
+ - [anything that should have been done but wasn't]
57
+
58
+ ### Suggestions
59
+ - [optional improvements, not blockers]
60
+ ```
61
+
62
+ ## Rules
63
+
64
+ - Be thorough but concise
65
+ - Distinguish between blockers (FAIL) and suggestions (PASS with notes)
66
+ - Never approve a change that introduces obvious bugs or security issues
67
+ - Flag missing test coverage explicitly
68
+ - Do not rewrite the code — only identify issues
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: refactor-reviewer
3
+ description: Inspect refactor scope, hidden impact, and risky omissions. Use during or after refactoring to catch missed references and breaking changes.
4
+ model: sonnet
5
+ tools:
6
+ - Read
7
+ - Grep
8
+ - Glob
9
+ - Bash
10
+ ---
11
+
12
+ # Refactor Reviewer
13
+
14
+ You are a refactoring review agent. Your job is to verify that a refactoring operation is complete and safe — catching missed references, broken imports, and hidden impact.
15
+
16
+ ## Input
17
+
18
+ You will receive:
19
+ - **What was refactored** (rename, move, extract, restructure)
20
+ - **List of changed files**
21
+ - **The old and new names/paths/structure**
22
+
23
+ ## Process
24
+
25
+ 1. **Search for stale references** to the old name/path/structure:
26
+ - Grep for old symbol names, old import paths, old file references
27
+ - Check string literals, comments, documentation, config files
28
+ - Check test files for old references
29
+
30
+ 2. **Verify new references are correct**:
31
+ - All imports resolve
32
+ - All type references are valid
33
+ - Re-exports are updated if applicable
34
+
35
+ 3. **Check for hidden consumers**:
36
+ - External APIs or CLI commands that reference the old structure
37
+ - Dynamic references (string-based imports, reflection)
38
+ - Build scripts, CI configs, documentation
39
+
40
+ 4. **Assess breaking change risk**:
41
+ - Is this a public API change?
42
+ - Are there downstream consumers?
43
+ - Is there a migration path?
44
+
45
+ ## Output Format
46
+
47
+ ```
48
+ ## Refactor Review Report
49
+
50
+ ### Summary
51
+ [1-2 sentence assessment]
52
+
53
+ ### Verdict: CLEAN | STALE_REFS | BREAKING
54
+
55
+ ### Stale References Found
56
+ | File | Line | Reference | Status |
57
+ |------|------|-----------|--------|
58
+ | path | N | old_name | needs update / false positive |
59
+
60
+ ### Breaking Changes
61
+ - [list of breaking changes, if any]
62
+
63
+ ### Hidden Impact
64
+ - [any indirect effects discovered]
65
+
66
+ ### Verification Commands
67
+ ```bash
68
+ # Commands to verify the refactor is complete
69
+ [specific build/test/lint commands]
70
+ ```
71
+
72
+ ### Confidence: [high/medium/low]
73
+ ```
74
+
75
+ ## Rules
76
+
77
+ - Always search for the **old** name/path in the entire codebase
78
+ - Check at least: source code, tests, docs, configs, CI files
79
+ - Distinguish between actual stale references and false positives (e.g., in comments describing history)
80
+ - Never approve a refactor without verifying no stale imports remain
81
+ - If the scope is very large (>20 files), focus on the highest-risk areas first
@@ -0,0 +1,109 @@
1
+ // OCO Hooks — Shared utilities (cross-platform: Windows, Linux, macOS)
2
+ import { createHash } from 'node:crypto';
3
+ import { execSync, execFileSync } from 'node:child_process';
4
+ import { existsSync, mkdirSync, readFileSync, readSync, writeFileSync, appendFileSync, lstatSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { homedir, tmpdir } from 'node:os';
7
+
8
+ /** Read JSON from stdin (Claude Code pipes hook input). */
9
+ export function readStdin() {
10
+ try {
11
+ const chunks = [];
12
+ const fd = 0; // stdin
13
+ const buf = Buffer.alloc(65536);
14
+ let n;
15
+ try { while ((n = readSync(fd, buf)) > 0) chunks.push(buf.slice(0, n)); } catch {}
16
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
17
+ } catch { return {}; }
18
+ }
19
+
20
+ /** Async version using process.stdin */
21
+ export async function readStdinAsync() {
22
+ return new Promise((resolve) => {
23
+ let data = '';
24
+ let resolved = false;
25
+ const done = () => {
26
+ if (resolved) return;
27
+ resolved = true;
28
+ try { resolve(JSON.parse(data)); } catch { resolve({}); }
29
+ };
30
+ process.stdin.setEncoding('utf8');
31
+ process.stdin.on('data', (chunk) => { data += chunk; });
32
+ process.stdin.on('end', done);
33
+ process.stdin.on('error', done);
34
+ // Timeout after 3s in case stdin never closes
35
+ setTimeout(done, 3000);
36
+ });
37
+ }
38
+
39
+ /** Get a stable session state directory, cross-platform. */
40
+ export function getStateDir() {
41
+ let workspaceRoot;
42
+ try {
43
+ workspaceRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
44
+ } catch {
45
+ workspaceRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
46
+ }
47
+
48
+ const hash = createHash('md5').update(workspaceRoot).digest('hex').slice(0, 12);
49
+ const cacheRoot = process.env.XDG_RUNTIME_DIR || join(homedir(), '.cache', 'oco');
50
+
51
+ const stateDir = join(cacheRoot, `session-${hash}`);
52
+ try {
53
+ mkdirSync(stateDir, { recursive: true });
54
+ // Basic symlink guard
55
+ if (lstatSync(stateDir).isSymbolicLink()) return join(tmpdir(), 'oco-fallback');
56
+ } catch {}
57
+
58
+ return stateDir;
59
+ }
60
+
61
+ /** Check if a command exists on PATH. */
62
+ export function commandExists(cmd) {
63
+ try {
64
+ const check = process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
65
+ execSync(check, { stdio: ['pipe', 'pipe', 'pipe'] });
66
+ return true;
67
+ } catch { return false; }
68
+ }
69
+
70
+ /** Run OCO CLI command and return parsed JSON. Degrades gracefully. */
71
+ export function ocoRun(args) {
72
+ const bin = process.env.OCO_BIN || 'oco';
73
+ if (!commandExists(bin)) return null;
74
+ try {
75
+ const result = execFileSync(bin, args, {
76
+ encoding: 'utf8',
77
+ timeout: 5000,
78
+ stdio: ['pipe', 'pipe', 'pipe'],
79
+ });
80
+ return JSON.parse(result);
81
+ } catch { return null; }
82
+ }
83
+
84
+ /** Read a state file (returns content or default). */
85
+ export function readState(stateDir, filename, defaultVal = '') {
86
+ const p = join(stateDir, filename);
87
+ try { return readFileSync(p, 'utf8').trim(); } catch { return defaultVal; }
88
+ }
89
+
90
+ /** Write a state file. */
91
+ export function writeState(stateDir, filename, content) {
92
+ try { writeFileSync(join(stateDir, filename), String(content)); } catch {}
93
+ }
94
+
95
+ /** Append to a state file. */
96
+ export function appendState(stateDir, filename, content) {
97
+ try { appendFileSync(join(stateDir, filename), content + '\n'); } catch {}
98
+ }
99
+
100
+ /** Output structured hook response as JSON. */
101
+ export function respond(obj) {
102
+ process.stdout.write(JSON.stringify(obj));
103
+ }
104
+
105
+ /** Write error to stderr (shown to Claude when exit 2). */
106
+ export function blockWith(message) {
107
+ process.stderr.write(message);
108
+ process.exit(2);
109
+ }
@@ -0,0 +1,88 @@
1
+ // OCO Hook: PostToolUse (cross-platform)
2
+ // Records observations, tracks modifications and verification timestamps.
3
+ // MUST exit within 4s no matter what.
4
+ import { execFile } from 'node:child_process';
5
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, lstatSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+ import { homedir, tmpdir } from 'node:os';
9
+
10
+ const killTimer = setTimeout(() => process.exit(0), 4000);
11
+ killTimer.unref();
12
+ process.on('uncaughtException', () => process.exit(0));
13
+ process.on('unhandledRejection', () => process.exit(0));
14
+
15
+ // --- Helpers (inlined) ---
16
+ function getStateDir() {
17
+ let root;
18
+ try { root = require('node:child_process').execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim(); } catch { root = process.env.CLAUDE_PROJECT_DIR || process.cwd(); }
19
+ const hash = createHash('md5').update(root).digest('hex').slice(0, 12);
20
+ const dir = join(process.env.XDG_RUNTIME_DIR || join(homedir(), '.cache', 'oco'), `session-${hash}`);
21
+ try { mkdirSync(dir, { recursive: true }); if (lstatSync(dir).isSymbolicLink()) return join(tmpdir(), 'oco-fallback'); } catch {}
22
+ return dir;
23
+ }
24
+ function readState(dir, file, def = '') { try { return readFileSync(join(dir, file), 'utf8').trim(); } catch { return def; } }
25
+ function writeState(dir, file, val) { try { writeFileSync(join(dir, file), String(val)); } catch {} }
26
+ function appendState(dir, file, val) { try { appendFileSync(join(dir, file), val + '\n'); } catch {} }
27
+
28
+ function readStdin() {
29
+ return new Promise((resolve) => {
30
+ let data = '', done = false;
31
+ const finish = () => { if (done) return; done = true; try { resolve(JSON.parse(data)); } catch { resolve(null); } };
32
+ process.stdin.setEncoding('utf8');
33
+ process.stdin.on('data', (c) => { data += c; });
34
+ process.stdin.on('end', finish);
35
+ process.stdin.on('error', finish);
36
+ setTimeout(finish, 1000);
37
+ });
38
+ }
39
+
40
+ try {
41
+ const input = await readStdin();
42
+ const toolName = input?.tool_name || '';
43
+ const toolError = input?.error || '';
44
+ if (!toolName) process.exit(0);
45
+
46
+ const stateDir = getStateDir();
47
+ const now = Math.floor(Date.now() / 1000);
48
+
49
+ // --- Telemetry: record observation (async, fire-and-forget) ---
50
+ try {
51
+ const bin = process.env.OCO_BIN || 'oco';
52
+ execFile(bin, ['observe', '--tool', toolName, '--status', toolError ? 'error' : 'ok', '--format', 'json'], { timeout: 3000, windowsHide: true });
53
+ } catch {}
54
+
55
+ // --- Track modified files ---
56
+ if (['Edit', 'Write', 'MultiEdit'].includes(toolName)) {
57
+ const filePath = input.tool_input?.file_path || input.tool_input?.path || input.tool_input?.destination || '';
58
+ if (filePath) {
59
+ appendState(stateDir, 'modified-files', filePath);
60
+ writeState(stateDir, 'last-modified-ts', String(now));
61
+ }
62
+ }
63
+
64
+ // --- Detect verification commands ---
65
+ if (!toolError && (toolName === 'Bash' || toolName === 'bash')) {
66
+ const command = (input.tool_input?.command || '').toLowerCase();
67
+ const verifyCmds = [
68
+ 'cargo test', 'cargo build', 'cargo check', 'cargo clippy',
69
+ 'npm test', 'npm run build', 'npm run lint',
70
+ 'pytest', 'python -m pytest', 'go test', 'go build',
71
+ 'tsc --noemit', 'npx tsc', 'mypy', 'ruff check',
72
+ ];
73
+ for (const vc of verifyCmds) {
74
+ if (command.startsWith(vc) || command.includes(` && ${vc}`) || command.includes(`; ${vc}`)) {
75
+ writeState(stateDir, 'verify-done', String(now));
76
+ break;
77
+ }
78
+ }
79
+ }
80
+
81
+ // --- Reset loop counter on success ---
82
+ if (!toolError) {
83
+ const loopFile = `loop-${toolName.replace(/[^a-zA-Z0-9]/g, '_')}`;
84
+ writeState(stateDir, loopFile, '0');
85
+ }
86
+ } catch {}
87
+
88
+ process.exit(0);
@@ -0,0 +1,100 @@
1
+ // OCO Hook: PreToolUse (cross-platform)
2
+ // Enforces tool policy gates before execution.
3
+ // MUST exit within 4s no matter what.
4
+ import { execFileSync } from 'node:child_process';
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, lstatSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+ import { homedir, tmpdir } from 'node:os';
9
+
10
+ const killTimer = setTimeout(() => process.exit(0), 4000);
11
+ killTimer.unref();
12
+ process.on('uncaughtException', () => process.exit(0));
13
+ process.on('unhandledRejection', () => process.exit(0));
14
+
15
+ // --- Helpers (inlined, no external deps) ---
16
+ function getStateDir() {
17
+ let root;
18
+ try { root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim(); } catch { root = process.env.CLAUDE_PROJECT_DIR || process.cwd(); }
19
+ const hash = createHash('md5').update(root).digest('hex').slice(0, 12);
20
+ const dir = join(process.env.XDG_RUNTIME_DIR || join(homedir(), '.cache', 'oco'), `session-${hash}`);
21
+ try { mkdirSync(dir, { recursive: true }); if (lstatSync(dir).isSymbolicLink()) return join(tmpdir(), 'oco-fallback'); } catch {}
22
+ return dir;
23
+ }
24
+ function readState(dir, file, def = '') { try { return readFileSync(join(dir, file), 'utf8').trim(); } catch { return def; } }
25
+ function writeState(dir, file, val) { try { writeFileSync(join(dir, file), String(val)); } catch {} }
26
+ function respond(obj) { process.stdout.write(JSON.stringify(obj)); }
27
+ function blockWith(msg) { process.stderr.write(msg); process.exit(2); }
28
+
29
+ function readStdin() {
30
+ return new Promise((resolve) => {
31
+ let data = '', done = false;
32
+ const finish = () => { if (done) return; done = true; try { resolve(JSON.parse(data)); } catch { resolve(null); } };
33
+ process.stdin.setEncoding('utf8');
34
+ process.stdin.on('data', (c) => { data += c; });
35
+ process.stdin.on('end', finish);
36
+ process.stdin.on('error', finish);
37
+ setTimeout(finish, 1000);
38
+ });
39
+ }
40
+
41
+ try {
42
+ const input = await readStdin();
43
+ const toolName = input?.tool_name || '';
44
+ const toolInput = input?.tool_input || {};
45
+ if (!toolName) process.exit(0);
46
+
47
+ const stateDir = getStateDir();
48
+
49
+ // --- Destructive command detection ---
50
+ if (toolName === 'Bash' || toolName === 'bash') {
51
+ const command = (toolInput.command || '').toLowerCase();
52
+ const destructive = [
53
+ 'rm -rf', 'rm -r ', 'rmdir',
54
+ 'git reset --hard', 'git push --force', 'git push -f ',
55
+ 'git clean -fd', 'git checkout -- .', 'git restore .',
56
+ 'drop table', 'drop database', 'truncate table',
57
+ ];
58
+ for (const pattern of destructive) {
59
+ if (command.includes(pattern)) {
60
+ blockWith(`OCO policy: destructive command detected (${pattern}). Use a safer alternative or confirm explicitly.`);
61
+ }
62
+ }
63
+ }
64
+
65
+ // --- Sensitive file protection ---
66
+ if (['Edit', 'Write', 'MultiEdit'].includes(toolName)) {
67
+ const filePath = (toolInput.file_path || toolInput.path || '').toLowerCase();
68
+ const sensitive = ['.env', 'credentials', 'secrets', '.key', '.pem', 'id_rsa'];
69
+ for (const pattern of sensitive) {
70
+ if (filePath.includes(pattern)) {
71
+ blockWith(`OCO policy: write to sensitive file (${pattern}) blocked. Review manually.`);
72
+ }
73
+ }
74
+ }
75
+
76
+ // --- Loop detection ---
77
+ const loopFile = `loop-${toolName.replace(/[^a-zA-Z0-9]/g, '_')}`;
78
+ let count = parseInt(readState(stateDir, loopFile, '0'), 10) + 1;
79
+ writeState(stateDir, loopFile, String(count));
80
+
81
+ if (count >= 5) {
82
+ if (count >= 8) writeState(stateDir, loopFile, '0');
83
+ respond({ hookSpecificOutput: { additionalContext: `OCO: tool '${toolName}' called ${count} times. Possible loop — consider a different approach.` } });
84
+ process.exit(0);
85
+ }
86
+
87
+ // --- OCO advanced gate check (optional, fire-and-forget on failure) ---
88
+ try {
89
+ const bin = process.env.OCO_BIN || 'oco';
90
+ const raw = execFileSync(bin, ['gate-check', '--tool', toolName, '--input', JSON.stringify(toolInput), '--format', 'json'], {
91
+ encoding: 'utf8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
92
+ });
93
+ const gateResult = JSON.parse(raw);
94
+ if (gateResult?.decision === 'deny') {
95
+ blockWith(`OCO policy: ${gateResult.reason || 'denied by policy'}`);
96
+ }
97
+ } catch {}
98
+ } catch {}
99
+
100
+ process.exit(0);
@@ -0,0 +1,98 @@
1
+ // OCO Hook: Stop (cross-platform)
2
+ // Prevents premature completion when code was modified without verification.
3
+ // MUST exit within 4s no matter what.
4
+ import { execFileSync } from 'node:child_process';
5
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, lstatSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+ import { homedir, tmpdir } from 'node:os';
9
+
10
+ const killTimer = setTimeout(() => process.exit(0), 4000);
11
+ killTimer.unref();
12
+ process.on('uncaughtException', () => process.exit(0));
13
+ process.on('unhandledRejection', () => process.exit(0));
14
+
15
+ // --- Helpers (inlined) ---
16
+ function getStateDir() {
17
+ let root;
18
+ try { root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim(); } catch { root = process.env.CLAUDE_PROJECT_DIR || process.cwd(); }
19
+ const hash = createHash('md5').update(root).digest('hex').slice(0, 12);
20
+ const dir = join(process.env.XDG_RUNTIME_DIR || join(homedir(), '.cache', 'oco'), `session-${hash}`);
21
+ try { mkdirSync(dir, { recursive: true }); if (lstatSync(dir).isSymbolicLink()) return join(tmpdir(), 'oco-fallback'); } catch {}
22
+ return dir;
23
+ }
24
+ function readState(dir, file, def = '') { try { return readFileSync(join(dir, file), 'utf8').trim(); } catch { return def; } }
25
+
26
+ function readStdin() {
27
+ return new Promise((resolve) => {
28
+ let data = '', done = false;
29
+ const finish = () => { if (done) return; done = true; try { resolve(JSON.parse(data)); } catch { resolve(null); } };
30
+ process.stdin.setEncoding('utf8');
31
+ process.stdin.on('data', (c) => { data += c; });
32
+ process.stdin.on('end', finish);
33
+ process.stdin.on('error', finish);
34
+ setTimeout(finish, 1000);
35
+ });
36
+ }
37
+
38
+ try {
39
+ const input = await readStdin();
40
+ const stopReason = input?.reason || 'complete';
41
+
42
+ // Only enforce verification for completion stops
43
+ if (stopReason !== 'complete' && stopReason !== '') process.exit(0);
44
+
45
+ const stateDir = getStateDir();
46
+
47
+ // Check if files were modified during this session
48
+ const modifiedLog = join(stateDir, 'modified-files');
49
+ if (!existsSync(modifiedLog)) process.exit(0);
50
+
51
+ let modifiedFiles;
52
+ try {
53
+ modifiedFiles = [...new Set(readFileSync(modifiedLog, 'utf8').split('\n').filter(Boolean))];
54
+ } catch { process.exit(0); }
55
+
56
+ if (modifiedFiles.length === 0) process.exit(0);
57
+
58
+ // Ignore non-source files (hooks, configs, docs) — no verification needed
59
+ // Paths may be absolute; match against full path patterns
60
+ const nonSourcePatterns = [/[/\\]\.claude[/\\]/, /[/\\]\.github[/\\]/, /[/\\]docs[/\\]/, /\.md$/i, /\.json$/i, /\.ya?ml$/i, /\.toml$/i, /\.mjs$/i, /\.cjs$/i];
61
+ const sourceFiles = modifiedFiles.filter(f => !nonSourcePatterns.some(p => p.test(f)));
62
+ if (sourceFiles.length === 0) process.exit(0);
63
+
64
+ // Check verification timestamp vs last modification
65
+ const verifyTs = parseInt(readState(stateDir, 'verify-done', '0'), 10);
66
+ const modifiedTs = parseInt(readState(stateDir, 'last-modified-ts', '0'), 10);
67
+
68
+ if (verifyTs >= modifiedTs && verifyTs > 0) {
69
+ // Verification happened after last modification — clean and allow
70
+ try {
71
+ unlinkSync(modifiedLog);
72
+ unlinkSync(join(stateDir, 'verify-done'));
73
+ unlinkSync(join(stateDir, 'last-modified-ts'));
74
+ } catch {}
75
+ process.exit(0);
76
+ }
77
+
78
+ // Determine needed checks from project manifests
79
+ const cwd = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
80
+ const checks = [];
81
+ if (existsSync(join(cwd, 'Cargo.toml'))) checks.push('build', 'test', 'clippy');
82
+ if (existsSync(join(cwd, 'package.json'))) {
83
+ try {
84
+ const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
85
+ if (pkg.scripts?.test) checks.push('test');
86
+ if (pkg.scripts?.lint) checks.push('lint');
87
+ } catch { checks.push('test'); }
88
+ }
89
+ if (existsSync(join(cwd, 'pyproject.toml')) || existsSync(join(cwd, 'setup.py'))) checks.push('test', 'typecheck');
90
+
91
+ if (checks.length === 0) process.exit(0);
92
+
93
+ const fileList = sourceFiles.slice(0, 10).join(',');
94
+ process.stderr.write(`OCO: ${sourceFiles.length} file(s) modified [${fileList}] but no verification run detected. Recommended checks: ${checks.join(',')}. Run build/test/lint before completing.`);
95
+ process.exit(2);
96
+ } catch {}
97
+
98
+ process.exit(0);
@@ -0,0 +1,61 @@
1
+ // OCO Hook: UserPromptSubmit (CommonJS, callback-based, no deadlock)
2
+ // Non-blocking stdin — avoids readSync deadlock on Windows.
3
+ // ALWAYS writes JSON to stdout — empty stdout triggers false "hook error" in Claude Code.
4
+ // See: https://github.com/anthropics/claude-code/issues/36948
5
+ 'use strict';
6
+
7
+ const EMPTY = JSON.stringify({});
8
+
9
+ const killTimer = setTimeout(() => { process.stdout.write(EMPTY); process.exit(0); }, 3000);
10
+ killTimer.unref();
11
+ process.on('uncaughtException', () => { process.stdout.write(EMPTY); process.exit(0); });
12
+ process.on('unhandledRejection', () => { process.stdout.write(EMPTY); process.exit(0); });
13
+
14
+ let data = '';
15
+ let handled = false;
16
+
17
+ function done(json) {
18
+ if (handled) return;
19
+ handled = true;
20
+ process.stdout.write(json || EMPTY);
21
+ process.exit(0);
22
+ }
23
+
24
+ function handle() {
25
+ try {
26
+ const input = data ? JSON.parse(data) : null;
27
+ if (!input || !input.prompt) return done();
28
+
29
+ const prompt = input.prompt.toLowerCase();
30
+
31
+ const highPatterns = /\b(refactor|rewrite|migrate|redesign|overhaul|rearchitect|restructure)\b/;
32
+ const criticalPatterns = /\b(delete.*prod|drop.*database|reset.*hard|force.*push|rm\s+-rf)\b/;
33
+ const verifyPatterns = /\b(fix|bug|patch|repair|resolve|debug|test|build|deploy)\b/;
34
+
35
+ let complexity = 'medium';
36
+ if (criticalPatterns.test(prompt)) complexity = 'critical';
37
+ else if (highPatterns.test(prompt)) complexity = 'high';
38
+ else if (prompt.length < 30 && !verifyPatterns.test(prompt)) complexity = 'trivial';
39
+
40
+ if (complexity === 'trivial') return done();
41
+
42
+ const needsVerify = verifyPatterns.test(prompt) || complexity === 'high' || complexity === 'critical';
43
+
44
+ let guidance = '[OCO] complexity=' + complexity + ' verify=' + needsVerify;
45
+ if (complexity === 'high' || complexity === 'critical') {
46
+ guidance += ' | Recommended: investigate before acting.';
47
+ } else if (needsVerify) {
48
+ guidance += ' | Verify after changes.';
49
+ }
50
+
51
+ done(JSON.stringify({ hookSpecificOutput: { additionalContext: guidance } }));
52
+ } catch (e) {
53
+ done();
54
+ }
55
+ }
56
+
57
+ process.stdin.setEncoding('utf8');
58
+ process.stdin.on('data', (chunk) => { data += chunk; });
59
+ process.stdin.on('end', handle);
60
+ process.stdin.on('error', () => done());
61
+ setTimeout(handle, 500);