mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix(observe): 5-layer automated session guard to prevent self-loop observations (#399)
* 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 <affaan@dcube.ai>
This commit is contained in:
@@ -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=$!
|
||||
|
||||
(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user