From c52a28ace9e7e84c00309fc7b629955dfc46ecf9 Mon Sep 17 00:00:00 2001 From: ispaydeu <31388982+ispaydeu@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:40:03 -0400 Subject: [PATCH] fix(observe): 5-layer automated session guard to prevent self-loop observations (#399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(observe): add 5-layer automated session guard to prevent self-loop observations observe.sh currently fires for ALL hook events including automated/programmatic sessions: the ECC observer's own Haiku analysis runs, claude-mem observer sessions, CI pipelines, and any other tool that spawns `claude --print`. This causes an infinite feedback loop where automated sessions generate observations that trigger more automated analysis, burning Haiku tokens with no human activity. Add a 5-layer guard block after the `disabled` check: Layer 1: agent_id payload field — only present in subagent hooks; skip any subagent-scoped session (always automated by definition). Layer 2: CLAUDE_CODE_ENTRYPOINT env var — Claude Code sets this to sdk-ts, sdk-py, sdk-cli, mcp, or remote for programmatic/SDK invocations. Skip if any non-cli entrypoint is detected. This is universal: catches any tool using the Anthropic SDK without requiring tool cooperation. Layer 3: ECC_HOOK_PROFILE=minimal — existing ECC mechanism; respect it here to suppress non-essential hooks in observer contexts. Layer 4: ECC_SKIP_OBSERVE=1 — cooperative env var any external tool can set before spawning automated sessions (explicit opt-out contract). Layer 5: CWD path exclusions — skip sessions whose working directory matches known observer-session path patterns. Configurable via ECC_OBSERVE_SKIP_PATHS (comma-separated substrings, default: "observer-sessions,.claude-mem"). Also fix observer-loop.sh to set ECC_SKIP_OBSERVE=1 and ECC_HOOK_PROFILE=minimal before spawning the Haiku analysis subprocess, making the observer loop self-aware and closing the ECC→ECC self-observation loop without needing external coordination. Fixes: observe.sh fires unconditionally on automated sessions (#398) * fix(observe): address review feedback — reorder guards cheapest-first, fix empty pattern bug Two issues flagged by Copilot and CodeRabbit in PR #399: 1. Layer ordering: the agent_id check spawns a Python subprocess but ran before the cheap env-var checks (CLAUDE_CODE_ENTRYPOINT, ECC_HOOK_PROFILE, ECC_SKIP_OBSERVE). Reorder to put all env-var checks first (Layers 1-3), then the subprocess-requiring agent_id check (Layer 4). Automated sessions that set env vars — the common case — now exit without spawning Python. 2. Empty pattern bug in Layer 5: if ECC_OBSERVE_SKIP_PATHS contains a trailing comma or spaces after commas (e.g. "path1, path2" or "path1,"), _pattern becomes empty or whitespace-only, and the glob *""* matches every CWD, silently disabling all observations. Fix: trim leading/trailing whitespace from each pattern and skip empty patterns with `continue`. * fix: fail closed for non-cli entrypoints --------- Co-authored-by: Affaan Mustafa --- .../agents/observer-loop.sh | 3 +- .../continuous-learning-v2/hooks/observe.sh | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/skills/continuous-learning-v2/agents/observer-loop.sh b/skills/continuous-learning-v2/agents/observer-loop.sh index f4aca82b..a0f655dd 100755 --- a/skills/continuous-learning-v2/agents/observer-loop.sh +++ b/skills/continuous-learning-v2/agents/observer-loop.sh @@ -91,7 +91,8 @@ PROMPT max_turns=10 fi - claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 & + # Prevent observe.sh from recording this automated Haiku session as observations + ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 & claude_pid=$! ( diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 33ec6f04..90a4a557 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -98,6 +98,54 @@ if [ -f "$CONFIG_DIR/disabled" ]; then exit 0 fi +# ───────────────────────────────────────────── +# Automated session guards +# Prevents observe.sh from firing on non-human sessions to avoid: +# - ECC observing its own Haiku observer sessions (self-loop) +# - ECC observing other tools' automated sessions (e.g. claude-mem) +# - All-night Haiku usage with no human activity +# ───────────────────────────────────────────── + +# Env-var checks first (cheapest — no subprocess spawning): + +# Layer 1: CLAUDE_CODE_ENTRYPOINT — set by Claude Code itself to indicate how +# it was invoked. Only interactive terminal sessions should continue; treat any +# explicit non-cli entrypoint as automated so future entrypoint types fail closed +# without requiring updates here. +case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in + cli) ;; + *) exit 0 ;; +esac + +# Layer 2: Respect ECC_HOOK_PROFILE=minimal — suppresses non-essential hooks +[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0 + +# Layer 3: Cooperative skip env var — tools like claude-mem can set this +# (export ECC_SKIP_OBSERVE=1) before spawning their automated sessions +[ "${ECC_SKIP_OBSERVE:-0}" = "1" ] && exit 0 + +# Layer 4: Skip subagent sessions — agent_id is only present when a hook fires +# inside a subagent (automated by definition, never a human interactive session). +# Placed after env-var checks to avoid a Python subprocess on sessions that +# already exit via Layers 1-3. +_ECC_AGENT_ID=$(echo "$INPUT_JSON" | "$PYTHON_CMD" -c "import json,sys; print(json.load(sys.stdin).get('agent_id',''))" 2>/dev/null || true) +[ -n "$_ECC_AGENT_ID" ] && exit 0 + +# Layer 5: CWD path exclusions — skip known observer-session directories. +# Add custom paths via ECC_OBSERVE_SKIP_PATHS (comma-separated substrings). +# Whitespace is trimmed from each pattern; empty patterns are skipped to +# prevent an empty-string glob from matching every path. +_ECC_SKIP_PATHS="${ECC_OBSERVE_SKIP_PATHS:-observer-sessions,.claude-mem}" +if [ -n "$STDIN_CWD" ]; then + IFS=',' read -ra _ECC_SKIP_ARRAY <<< "$_ECC_SKIP_PATHS" + for _pattern in "${_ECC_SKIP_ARRAY[@]}"; do + _pattern="${_pattern#"${_pattern%%[![:space:]]*}"}" # trim leading whitespace + _pattern="${_pattern%"${_pattern##*[![:space:]]}"}" # trim trailing whitespace + [ -z "$_pattern" ] && continue + case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac + done +fi + # Auto-purge observation files older than 30 days (runs once per session) PURGE_MARKER="${PROJECT_DIR}/.last-purge" if [ ! -f "$PURGE_MARKER" ] || [ "$(find "$PURGE_MARKER" -mtime +1 2>/dev/null)" ]; then