Sync generated provider output

This commit is contained in:
github-actions[bot]
2026-08-09 01:43:56 +00:00
parent 490dcfd678
commit 5c8652b019
60 changed files with 540 additions and 120 deletions
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters — catches non-git projects. * within the first ~300 characters — catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters — catches non-git projects. * within the first ~300 characters — catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters — catches non-git projects. * within the first ~300 characters — catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters — catches non-git projects. * within the first ~300 characters — catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters — catches non-git projects. * within the first ~300 characters — catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters — catches non-git projects. * within the first ~300 characters — catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters — catches non-git projects. * within the first ~300 characters — catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }
+13 -1
View File
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
function quoteCommandArg(value) { function quoteCommandArg(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; // The suggestion is meant to be run on this same machine, so quote for its
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
// double quotes, and these values come from scanned file content (a
// font-family name) or a file path, so untrusted input must be
// single-quoted (issue #476). Windows cmd.exe performs no such command
// substitution, but it treats a single quote as a literal character rather
// than a grouping delimiter, so a value or path containing spaces has to
// stay double-quoted there (Greptile #533). Keep the pre-existing
// double-quote escaping on Windows so that path's behavior is unchanged.
if (process.platform === 'win32') {
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return `'${text.replace(/'/g, `'\\''`)}'`;
} }
function relativize(filePath, cwd) { function relativize(filePath, cwd) {
@@ -13,7 +13,7 @@
* within the first ~300 characters catches non-git projects. * within the first ~300 characters catches non-git projects.
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
function isGitIgnored(absPath, cwd) { function isGitIgnored(absPath, cwd) {
try { try {
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { // argv form, never a shell: this runs on every file the live-mode source
// walk reaches, so a hostile filename embedding $(...) or backticks must
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
execFileSync('git', ['check-ignore', '--quiet', absPath], {
cwd, cwd,
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
// * bundle-relative: node ".agents/.../hook.mjs" // * bundle-relative: node ".agents/.../hook.mjs"
// * legacy unquoted: node .claude/.../hook.mjs // * legacy unquoted: node .claude/.../hook.mjs
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
// * absolute: node "/Users/.../hook.mjs" (user-level installs) // * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
// the shell-injection fix; older installs double-quote)
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
// A quoted path wins; the guard's two occurrences are identical, so the first // A quoted path wins; the guard's two occurrences are identical, so the first
// quoted match is the path. Otherwise fall back to the whitespace/metachar- // quoted match is the path. Otherwise fall back to the whitespace/metachar-
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
if (!HOOK_MARKER.test(str)) return null; if (!HOOK_MARKER.test(str)) return null;
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
if (quoted) return quoted[1]; if (quoted) return quoted[1];
// A path containing an apostrophe serializes as '\'' inside single quotes;
// no regex reassembles that, and the bare fallback would misread a fragment
// of it, so return null: the caller never asserts on a path it can't parse.
if (str.includes("'\\''")) return null;
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
if (singleQuoted) return singleQuoted[1];
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
return bare ? bare[1] : null; return bare ? bare[1] : null;
} }
+10 -4
View File
@@ -17,7 +17,7 @@
* node live.mjs --help * node live.mjs --help
*/ */
import { execSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
function runScript(name, args, options = {}) { function runScript(name, args, options = {}) {
const scriptPath = path.join(__dirname, name); const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); // argv form, never a shell: string interpolation into double quotes would
// let a `"` or `$(...)` in any future caller's arg escape into the shell
// (issue #476).
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: 'utf-8',
cwd: options.cwd || process.cwd(),
timeout: 15_000,
});
} catch (err) { } catch (err) {
// execSync throws on non-zero exit; return stdout if any // execFileSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || ''; return err.stdout || err.message || '';
} }
} }