diff --git a/.cursor/hooks/adapter.js b/.cursor/hooks/adapter.js index e1400d820..e019df446 100644 --- a/.cursor/hooks/adapter.js +++ b/.cursor/hooks/adapter.js @@ -15,11 +15,12 @@ const STOP_FORMAT_TYPECHECK_TIMEOUT_HEADROOM_MS = 15000; function readStdin(options = {}) { return new Promise((resolve) => { + const stream = options.stream || process.stdin; let data = ''; let dataBytes = 0; let truncated = false; - process.stdin.setEncoding('utf8'); - process.stdin.on('data', chunk => { + stream.setEncoding('utf8'); + stream.on('data', chunk => { const chunkBytes = Buffer.byteLength(chunk, 'utf8'); if (truncated || dataBytes + chunkBytes > MAX_STDIN_BYTES) { truncated = true; @@ -28,7 +29,7 @@ function readStdin(options = {}) { data += chunk; dataBytes += chunkBytes; }); - process.stdin.on('end', () => { + stream.on('end', () => { if (truncated) { if (options.includeMetadata !== true) { process.stderr.write( @@ -36,18 +37,18 @@ function readStdin(options = {}) { ); } resolve(options.includeMetadata === true - ? { raw: '', truncated: true } + ? { raw: '', truncated: true, readError: false } : ''); return; } resolve(options.includeMetadata === true - ? { raw: data, truncated: false } + ? { raw: data, truncated: false, readError: false } : data); }); - process.stdin.on('error', error => { + stream.on('error', error => { process.stderr.write(`[Cursor Hook] stdin read failed: ${error.message}\n`); resolve(options.includeMetadata === true - ? { raw: '', truncated: false } + ? { raw: '', truncated: false, readError: true } : ''); }); }); diff --git a/.cursor/hooks/before-read-file.js b/.cursor/hooks/before-read-file.js index 4b9bd8a80..9ecbcaada 100644 --- a/.cursor/hooks/before-read-file.js +++ b/.cursor/hooks/before-read-file.js @@ -7,7 +7,8 @@ readStdin().then(raw => { } try { const input = JSON.parse(raw); - const filePath = input.file_path || input.path || input.file || ''; + const filePath = [input.file_path, input.path, input.file] + .find(value => typeof value === 'string' && value.trim()) || ''; if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) { console.error('[ECC] WARNING: Reading sensitive file: ' + filePath); console.error('[ECC] Ensure this data is not exposed in outputs'); diff --git a/.cursor/hooks/before-shell-execution.js b/.cursor/hooks/before-shell-execution.js index 982ceccbd..bee100908 100644 --- a/.cursor/hooks/before-shell-execution.js +++ b/.cursor/hooks/before-shell-execution.js @@ -7,8 +7,8 @@ const { } = require('./adapter'); readStdin({ includeMetadata: true }) - .then(({ raw, truncated }) => { - if (truncated) { + .then(({ raw, truncated, readError }) => { + if (truncated || readError) { const hasBlockingGuard = ( hookEnabled('pre:bash:block-no-verify', ['minimal', 'standard', 'strict']) || ( @@ -17,12 +17,14 @@ readStdin({ includeMetadata: true }) ) ); if (hasBlockingGuard) { - console.error( - '[Cursor Hook] stdin exceeded the safety limit; blocking beforeShellExecution' - ); + console.error(truncated + ? '[Cursor Hook] stdin exceeded the safety limit; blocking beforeShellExecution' + : '[Cursor Hook] stdin read failed; blocking beforeShellExecution'); process.exit(2); } - console.error('[Cursor Hook] stdin exceeded the safety limit; suppressing truncated input'); + console.error(truncated + ? '[Cursor Hook] stdin exceeded the safety limit; suppressing truncated input' + : '[Cursor Hook] stdin read failed; suppressing untrusted input'); return; } diff --git a/.cursor/hooks/before-tab-file-read.js b/.cursor/hooks/before-tab-file-read.js index 57256dda6..2fcf1d079 100644 --- a/.cursor/hooks/before-tab-file-read.js +++ b/.cursor/hooks/before-tab-file-read.js @@ -1,17 +1,20 @@ #!/usr/bin/env node const { readStdin, hookEnabled } = require('./adapter'); -readStdin({ includeMetadata: true }).then(({ raw, truncated }) => { +readStdin({ includeMetadata: true }).then(({ raw, truncated, readError }) => { if (!hookEnabled('pre:tab-read:sensitive-file-block', ['minimal', 'standard', 'strict'])) { process.stdout.write(raw); return; } - if (truncated) { - console.error('[Cursor Hook] stdin exceeded the safety limit; blocking beforeTabFileRead'); + if (truncated || readError) { + console.error(truncated + ? '[Cursor Hook] stdin exceeded the safety limit; blocking beforeTabFileRead' + : '[Cursor Hook] stdin read failed; blocking beforeTabFileRead'); process.exit(2); } try { const input = JSON.parse(raw); - const filePath = input.file_path || input.path || input.file || ''; + const filePath = [input.file_path, input.path, input.file] + .find(value => typeof value === 'string' && value.trim()) || ''; if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) { console.error('[ECC] BLOCKED: Tab cannot read sensitive file: ' + filePath); process.exit(2); diff --git a/tests/hooks/cursor-hook-dispatchers.test.js b/tests/hooks/cursor-hook-dispatchers.test.js index 7d5919bc3..22a89be3d 100644 --- a/tests/hooks/cursor-hook-dispatchers.test.js +++ b/tests/hooks/cursor-hook-dispatchers.test.js @@ -287,6 +287,17 @@ if (test('Cursor-sensitive file hooks recognize the official file_path field', ( assert.match(blocked.stderr, /BLOCKED/); })) passed++; else failed++; +if (test('invalid path field types cannot shadow a valid string fallback', () => { + const input = { file_path: {}, path: '.env' }; + const warning = runHook(BEFORE_READ, input); + assert.strictEqual(warning.status, 0, warning.stderr); + assert.match(warning.stderr, /Reading sensitive file/); + + const blocked = runHook(BEFORE_TAB_READ, input); + assert.strictEqual(blocked.status, 2, blocked.stderr); + assert.match(blocked.stderr, /BLOCKED/); +})) passed++; else failed++; + if (test('Cursor-native audit hooks stay disabled in the minimal profile', () => { const cases = [ [BEFORE_MCP, { server: 'example', tool: 'read' }], @@ -661,6 +672,27 @@ if (test('oversized beforeTabFileRead input fails closed', () => { assert.match(result.stderr, /stdin exceeded.*blocking/i); })) passed++; else failed++; +if (test('readStdin exposes stream failures to blocking dispatchers', () => { + const probe = [ + "const { EventEmitter } = require('events');", + `const { readStdin } = require(${JSON.stringify(path.join(REPO_ROOT, '.cursor', 'hooks', 'adapter.js'))});`, + "const stream = new EventEmitter();", + "stream.setEncoding = () => {};", + "readStdin({ includeMetadata: true, stream }).then(result => process.stdout.write(JSON.stringify(result)));", + "setImmediate(() => stream.emit('error', new Error('synthetic read failure')));", + ].join('\n'); + const result = spawnSync(process.execPath, ['-e', probe], { + encoding: 'utf8', + timeout: 5000, + }); + assert.strictEqual(result.status, 0, result.stderr); + assert.deepStrictEqual(JSON.parse(result.stdout), { + raw: '', + truncated: false, + readError: true, + }); +})) passed++; else failed++; + console.log('-'.repeat(55)); console.log(`Passed: ${passed}`); console.log(`Failed: ${failed}`);