From d5dee31321910617f63eb117e126df53415ffa82 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Tue, 15 Sep 2026 00:29:23 +0530 Subject: [PATCH] fix(security): stderr skip, independent audit, home MCP trust, dry-run conflict check - pre-push: send skip diagnostic to stderr (not stdout) so consumers relying on stderr for warnings receive the message - pre-push: move ECC_PREPUSH_AUDIT outside RUN_CHECKS gate so audit-only configurations still check dependencies - mcp-health-check: classify home config paths as trusted before applying workspace opt-in gate; when cwd == home, ~/.claude.json was incorrectly blocked as untrusted workspace config - install-global-git-hooks: check conflicting global core.hooksPath in dry-run mode too, so dry-run accurately reflects what apply would do --- scripts/codex-git-hooks/pre-push | 5 ++-- scripts/codex/install-global-git-hooks.sh | 30 +++++++++++------------ scripts/hooks/mcp-health-check.js | 29 ++++++++++++++-------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/scripts/codex-git-hooks/pre-push b/scripts/codex-git-hooks/pre-push index f98f3ef7d..ed32c9369 100755 --- a/scripts/codex-git-hooks/pre-push +++ b/scripts/codex-git-hooks/pre-push @@ -89,7 +89,7 @@ if [[ -f "package.json" ]]; then # arbitrary code execution (package.json scripts run as you). Opt-in only: # set ECC_PREPUSH_RUN_CHECKS=1 for repos you trust. if [[ "${ECC_PREPUSH_RUN_CHECKS:-0}" != "1" ]]; then - log "Node project detected but ECC_PREPUSH_RUN_CHECKS!=1; skipping repo script execution (set =1 to opt in)." + printf '[ECC pre-push] Node project detected but ECC_PREPUSH_RUN_CHECKS!=1; skipping repo script execution (set =1 to opt in).\n' >&2 else pm="$(detect_pm)" log "Node project detected (package manager: $pm)" @@ -104,7 +104,9 @@ if [[ -f "package.json" ]]; then fi done + fi if [[ "${ECC_PREPUSH_AUDIT:-0}" == "1" ]]; then + pm="${pm:-$(detect_pm)}" ran_any_check=1 log "Running dependency audit (ECC_PREPUSH_AUDIT=1)" case "$pm" in @@ -115,7 +117,6 @@ if [[ -f "package.json" ]]; then *) npm audit --omit=dev || fail "npm audit failed" ;; esac fi - fi fi # SECURITY: go test / pytest execute repo-controlled code (TestMain, diff --git a/scripts/codex/install-global-git-hooks.sh b/scripts/codex/install-global-git-hooks.sh index 0c37c3c2c..22702a5f5 100755 --- a/scripts/codex/install-global-git-hooks.sh +++ b/scripts/codex/install-global-git-hooks.sh @@ -41,23 +41,21 @@ log "Mode: $MODE" log "Source hooks: $SOURCE_DIR" log "Global hooks destination: $DEST_DIR" -if [[ "$MODE" == "apply" ]]; then - prev_hooks_path="$(git config --global core.hooksPath || true)" - if [[ -n "$prev_hooks_path" && "$prev_hooks_path" != "$DEST_DIR" ]]; then - # SECURITY: never silently displace another tool's global hooks — that - # turns every commit/push in every repo into ECC code execution and breaks - # the user's existing security controls. Require explicit opt-in to replace. - if [[ "${ECC_FORCE_GLOBAL_HOOKS:-0}" != "1" ]]; then - log "ERROR: global core.hooksPath already set to: $prev_hooks_path" - log "Refusing to overwrite. Options:" - log " 1) Per-repo install (recommended): git config core.hooksPath \"$DEST_DIR\"" - log " 2) Force replace: ECC_FORCE_GLOBAL_HOOKS=1 $0" - log " 3) Restore afterwards: git config --global core.hooksPath \"$prev_hooks_path\"" - exit 1 - fi - log "WARNING: replacing previous global hooksPath: $prev_hooks_path (ECC_FORCE_GLOBAL_HOOKS=1)" - log "Restore with: git config --global core.hooksPath \"$prev_hooks_path\"" +prev_hooks_path="$(git config --global core.hooksPath || true)" +if [[ -n "$prev_hooks_path" && "$prev_hooks_path" != "$DEST_DIR" ]]; then + # SECURITY: never silently displace another tool's global hooks — that + # turns every commit/push in every repo into ECC code execution and breaks + # the user's existing security controls. Require explicit opt-in to replace. + if [[ "${ECC_FORCE_GLOBAL_HOOKS:-0}" != "1" ]]; then + log "ERROR: global core.hooksPath already set to: $prev_hooks_path" + log "Refusing to overwrite. Options:" + log " 1) Per-repo install (recommended): git config core.hooksPath \"$DEST_DIR\"" + log " 2) Force replace: ECC_FORCE_GLOBAL_HOOKS=1 $0" + log " 3) Restore afterwards: git config --global core.hooksPath \"$prev_hooks_path\"" + exit 1 fi + log "WARNING: replacing previous global hooksPath: $prev_hooks_path (ECC_FORCE_GLOBAL_HOOKS=1)" + log "Restore with: git config --global core.hooksPath \"$prev_hooks_path\"" fi if [[ -d "$DEST_DIR" ]]; then diff --git a/scripts/hooks/mcp-health-check.js b/scripts/hooks/mcp-health-check.js index edc28a6cc..843a6ec04 100644 --- a/scripts/hooks/mcp-health-check.js +++ b/scripts/hooks/mcp-health-check.js @@ -540,16 +540,25 @@ async function probeServer(serverName, resolvedConfig) { try { const src = String(resolvedConfig.source || ''); const cwd = process.cwd(); - const isWorkspaceSource = src === require('path').join(cwd, '.claude.json') - || src === require('path').join(cwd, '.claude', 'settings.json') - || src.startsWith(cwd + require('path').sep + '.claude' + require('path').sep); - if (isWorkspaceSource && !/^(1|true|yes)$/i.test(String(process.env.ECC_MCP_ALLOW_WORKSPACE_PROBE || ''))) { - return { - ok: false, - failureCode: null, - reason: 'untrusted workspace MCP config skipped (set ECC_MCP_ALLOW_WORKSPACE_PROBE=1 to probe)', - source: resolvedConfig.source - }; + const home = require('os').homedir(); + const pathMod = require('path'); + // A config file in the user's home directory (~/.claude.json or + // ~/.claude/settings.json) is always trusted regardless of cwd. + const isHomeSource = src === pathMod.join(home, '.claude.json') + || src === pathMod.join(home, '.claude', 'settings.json') + || src.startsWith(pathMod.join(home, '.claude') + pathMod.sep); + if (!isHomeSource) { + const isWorkspaceSource = src === pathMod.join(cwd, '.claude.json') + || src === pathMod.join(cwd, '.claude', 'settings.json') + || src.startsWith(cwd + pathMod.sep + '.claude' + pathMod.sep); + if (isWorkspaceSource && !/^(1|true|yes)$/i.test(String(process.env.ECC_MCP_ALLOW_WORKSPACE_PROBE || ''))) { + return { + ok: false, + failureCode: null, + reason: 'untrusted workspace MCP config skipped (set ECC_MCP_ALLOW_WORKSPACE_PROBE=1 to probe)', + source: resolvedConfig.source + }; + } } } catch { // Fail closed on path errors for workspace sources is handled below;