diff --git a/scripts/claw.js b/scripts/claw.js index 2f5ff618f..74dea0f81 100644 --- a/scripts/claw.js +++ b/scripts/claw.js @@ -108,7 +108,12 @@ function askClaude(systemPrompt, history, userMessage, model) { // same quoted-command-line pattern as scripts/hooks/mcp-health-check.js so // space-containing paths survive as single tokens. .ps1 is never executed // directly — fall through to bare `claude` (pre-change behavior) instead. - const quoteWin = token => (/[\s"&|<>^%!();]/.test(token) ? '"' + token.replace(/"/g, '""') + '"' : token); + // cmd.exe expands %NAME% even inside double-quoted strings, so reject + // percent-delimited executable paths rather than route them through the shell. + function quoteWinToken(token) { + if (/%/.test(token)) return null; + return /[\s"&|<>^();]/.test(token) ? '"' + token.replace(/"/g, '""') + '"' : token; + } let bin = 'claude'; let useShell = false; if (process.platform === 'win32') { @@ -124,6 +129,9 @@ function askClaude(systemPrompt, history, userMessage, model) { break; } } + if (useShell && quoteWinToken(bin) === null) { + useShell = false; + } } const spawnOpts = { input: fullPrompt, @@ -133,7 +141,7 @@ function askClaude(systemPrompt, history, userMessage, model) { timeout: 300000, }; const result = useShell - ? spawnSync([bin, ...args].map(quoteWin).join(' '), { ...spawnOpts, shell: true }) + ? spawnSync([bin, ...args].map(quoteWinToken).join(' '), { ...spawnOpts, shell: true }) : spawnSync(bin, args, { ...spawnOpts, shell: false }); if (result.error) { diff --git a/scripts/codex/install-global-git-hooks.sh b/scripts/codex/install-global-git-hooks.sh index 33a7daf8d..0c37c3c2c 100755 --- a/scripts/codex/install-global-git-hooks.sh +++ b/scripts/codex/install-global-git-hooks.sh @@ -41,17 +41,6 @@ log "Mode: $MODE" log "Source hooks: $SOURCE_DIR" log "Global hooks destination: $DEST_DIR" -if [[ -d "$DEST_DIR" ]]; then - log "Backing up existing hooks directory to $BACKUP_DIR" - run_or_echo mkdir -p "$BACKUP_DIR" - run_or_echo cp -R "$DEST_DIR" "$BACKUP_DIR/hooks" -fi - -run_or_echo mkdir -p "$DEST_DIR" -run_or_echo cp "$SOURCE_DIR/pre-commit" "$DEST_DIR/pre-commit" -run_or_echo cp "$SOURCE_DIR/pre-push" "$DEST_DIR/pre-push" -run_or_echo chmod +x "$DEST_DIR/pre-commit" "$DEST_DIR/pre-push" - if [[ "$MODE" == "apply" ]]; then prev_hooks_path="$(git config --global core.hooksPath || true)" if [[ -n "$prev_hooks_path" && "$prev_hooks_path" != "$DEST_DIR" ]]; then @@ -70,6 +59,17 @@ if [[ "$MODE" == "apply" ]]; then log "Restore with: git config --global core.hooksPath \"$prev_hooks_path\"" fi fi + +if [[ -d "$DEST_DIR" ]]; then + log "Backing up existing hooks directory to $BACKUP_DIR" + run_or_echo mkdir -p "$BACKUP_DIR" + run_or_echo cp -R "$DEST_DIR" "$BACKUP_DIR/hooks" +fi + +run_or_echo mkdir -p "$DEST_DIR" +run_or_echo cp "$SOURCE_DIR/pre-commit" "$DEST_DIR/pre-commit" +run_or_echo cp "$SOURCE_DIR/pre-push" "$DEST_DIR/pre-push" +run_or_echo chmod +x "$DEST_DIR/pre-commit" "$DEST_DIR/pre-push" run_or_echo git config --global core.hooksPath "$DEST_DIR" log "Installed ECC global git hooks." diff --git a/scripts/orchestrate-codex-worker.sh b/scripts/orchestrate-codex-worker.sh index 135a639e8..fde2485ff 100755 --- a/scripts/orchestrate-codex-worker.sh +++ b/scripts/orchestrate-codex-worker.sh @@ -54,12 +54,15 @@ write_status "running" "- Task file: \`$task_file\`" # rm -rf / exfiltration commands without confirmation. # Default to the most restrictive approval mode; allow an explicit operator # override only via env (e.g. ECC_CODEX_APPROVAL_MODE=on-request for trusted runs). -APPROVAL_MODE="${ECC_CODEX_APPROVAL_MODE:-never}" -case "$APPROVAL_MODE" in +# Codex profiles (-p) and approval policies (--ask-for-approval) are +# independent concepts. SECURITY: default to never approving untrusted +# tool execution; operators can override via env. +APPROVAL_POLICY="${ECC_CODEX_APPROVAL_POLICY:-never}" +case "$APPROVAL_POLICY" in never|on-request|on-failure) ;; *) - echo "[ECC worker] Refusing to run: unsupported ECC_CODEX_APPROVAL_MODE='$APPROVAL_MODE' (expected never|on-request|on-failure)" >&2 - write_status "failed" "- Error: unsupported approval mode" + echo "[ECC worker] Refusing to run: unsupported ECC_CODEX_APPROVAL_POLICY='$APPROVAL_POLICY' (expected never|on-request|on-failure)" >&2 + write_status "failed" "- Error: unsupported approval policy" exit 1 ;; esac @@ -106,7 +109,7 @@ Task file: $task_file $(cat "$task_file") EOF -if codex exec -p "$APPROVAL_MODE" -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then +if codex exec --ask-for-approval "$APPROVAL_POLICY" -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then { echo "# Handoff" echo diff --git a/tests/scripts/codex-hooks.test.js b/tests/scripts/codex-hooks.test.js index 0dfe1d2f9..53c884c06 100644 --- a/tests/scripts/codex-hooks.test.js +++ b/tests/scripts/codex-hooks.test.js @@ -169,6 +169,7 @@ function runHermeticPrePush({ includeCorepack = true, includePnpm = false, audit = false, + runChecks = true, } = {}) { const tempDir = createTempDir('codex-pre-push-'); const binDir = path.join(tempDir, 'bin'); @@ -204,6 +205,7 @@ ${includePnpm ? functionStub('pnpm', false) : ''} PATH: toBashPath(binDir), BASH_ENV: toBashPath(bashEnv), ECC_PREPUSH_AUDIT: audit ? '1' : '0', + ECC_PREPUSH_RUN_CHECKS: runChecks ? '1' : '0', ECC_SKIP_GIT_HOOKS: '0', ECC_SKIP_PREPUSH: '0', MSYS_NO_PATHCONV: '1', @@ -221,7 +223,7 @@ ${includePnpm ? functionStub('pnpm', false) : ''} if ( test('pre-push uses Corepack pinned pnpm and runs every required verification script', () => { - const { result, calls } = runHermeticPrePush(); + const { result, calls } = runHermeticPrePush({ runChecks: true }); assert.strictEqual(result.status, 0, JSON.stringify(result, null, 2)); assert.deepStrictEqual(calls, [ 'pnpm run lint', @@ -264,7 +266,7 @@ else failed++; if ( test('pre-push stops immediately when a required verification script fails', () => { - const { result, calls } = runHermeticPrePush({ failScript: 'typecheck' }); + const { result, calls } = runHermeticPrePush({ runChecks: true, failScript: 'typecheck' }); assert.notStrictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); assert.deepStrictEqual(calls, ['pnpm run lint', 'pnpm run typecheck']); assert.match(result.stderr, /typecheck failed/); @@ -273,9 +275,20 @@ if ( passed++; else failed++; +if ( + test('pre-push skips verification scripts by default when opt-in is not set', () => { + const { result, calls } = runHermeticPrePush({ runChecks: false }); + assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepStrictEqual(calls, []); + assert.match(result.stderr, /ECC_PREPUSH_RUN_CHECKS!=1/); + }) +) + passed++; +else failed++; + if ( test('pre-push runs the production audit through Corepack pnpm', () => { - const { result, calls } = runHermeticPrePush({ audit: true }); + const { result, calls } = runHermeticPrePush({ runChecks: true, audit: true }); assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); assert.deepStrictEqual(calls, [ 'pnpm run lint',