Skip to content

fix(observe): 5-layer automated session guard to prevent self-loop observations#399

Merged
affaan-m merged 3 commits into
affaan-m:mainfrom
ispaydeu:fix/observer-automated-session-guards
Mar 13, 2026
Merged

fix(observe): 5-layer automated session guard to prevent self-loop observations#399
affaan-m merged 3 commits into
affaan-m:mainfrom
ispaydeu:fix/observer-automated-session-guards

Conversation

@ispaydeu

@ispaydeu ispaydeu commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Problem

observe.sh fires for all PostToolUse hook events indiscriminately, including automated/programmatic sessions:

  • ECC self-loop: The observer's own Haiku analysis runs trigger new observations, which trigger more analysis. With a busy project, this creates an infinite cycle burning Haiku tokens overnight with no human activity.
  • Third-party tools: claude-mem, CI pipelines, and any other tool that spawns claude --print are treated as observation-worthy human sessions.
  • No signals available: CLAUDECODE=1 is often unset by automated callers to bypass the nested session guard, making it useless for detection. There was no reliable way to filter automated sessions.

Verified in the wild: ~/.claude/homunculus/projects/*/observer.log showed 2,534+ consecutive Error: Reached max turns (3) entries — every single analysis attempt failed — yet the observation loop kept running and re-triggering.

Solution

Add a 5-layer guard block immediately after the disabled file check in observe.sh. Each layer catches a different class of automated session:

Layer 1: agent_id payload field

The PostToolUse JSON payload includes agent_id only when the hook fires inside a subagent (Task tool dispatch). Subagents are automated by definition — skip them.

_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 2: CLAUDE_CODE_ENTRYPOINT env var (universal — no tool cooperation needed)

Claude Code itself sets CLAUDE_CODE_ENTRYPOINT to the invocation method:

  • cli → interactive terminal (human session — observe this)
  • sdk-ts, sdk-py, sdk-cli → programmatic SDK invocation (skip)
  • mcp, remote → MCP server / remote session (skip)

This catches any tool using the Anthropic SDK without requiring that tool to set any special env vars. Works universally for claude-mem, CI runners, custom scripts, and future tools not yet written.

case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
  sdk-ts|sdk-py|sdk-cli|mcp|remote) exit 0 ;;
esac

Layer 3: ECC_HOOK_PROFILE=minimal

Existing ECC mechanism — respect it in observe.sh to suppress non-essential hooks when the observer itself sets a minimal profile.

Layer 4: ECC_SKIP_OBSERVE=1

Cooperative env var contract: any external tool can export ECC_SKIP_OBSERVE=1 before spawning automated sessions to explicitly opt out. Documents a clear API for tool authors.

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 includes observer-sessions and .claude-mem.

Self-referential fix in observer-loop.sh

The Haiku subprocess spawned by observer-loop.sh needs to close the ECC→ECC loop without depending on CLAUDE_CODE_ENTRYPOINT (which may be cli if invoked via raw subprocess). Set both env vars explicitly before the claude invocation:

ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model haiku --max-turns "$max_turns" --print < "$prompt_file" >> "$LOG_FILE" 2>&1 &

Why multiple layers?

No single signal covers all cases:

  • agent_id: only present for subagents, not top-level claude --print
  • CLAUDE_CODE_ENTRYPOINT: catches SDK callers, but raw subprocess invocations still show cli
  • ECC_HOOK_PROFILE: requires ECC to set it (covered by observer-loop fix)
  • ECC_SKIP_OBSERVE: requires caller cooperation (covered by observer-loop fix; available for any tool)
  • CWD exclusions: catches known path patterns as a last-resort fallback

The layers are ordered cheapest-first (env var checks before subprocess spawning).

Test plan

  • Interactive terminal session: all 5 checks pass through, observation recorded normally
  • ECC Haiku analysis subprocess: Layer 3 + 4 both exit early (no self-observation)
  • claude --print via Anthropic Python SDK: Layer 2 exits early on sdk-py
  • Task tool subagent: Layer 1 exits early on agent_id
  • ECC_SKIP_OBSERVE=1 set by third-party tool: Layer 4 exits early
  • CWD = ~/.claude-mem/observer-session/xyz: Layer 5 exits early
  • Observer log shows no "self-observation" entries after restart

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Added multiple runtime guards to avoid recording or observing automated, non-interactive, or programmatic sessions (environmental/profile checks, input-based detection, and path exclusions), preventing unnecessary observation subprocesses and skipping designated paths to improve efficiency and privacy.
  • Documentation
    • Added comments explaining the new guard layers and their behavior.

Summary by cubic

Prevents self-loop observations and automated session noise by adding a 5-layer guard in observe.sh and making the observer loop opt out by default. Fails closed for any non-cli CLAUDE_CODE_ENTRYPOINT to catch future automated entrypoints.

  • Bug Fixes
    • Added a 5-layer guard in observe.sh, ordered cheapest-first: CLAUDE_CODE_ENTRYPOINT (fail closed for any non-cli), ECC_HOOK_PROFILE=minimal, ECC_SKIP_OBSERVE=1, agent_id, and CWD exclusions via ECC_OBSERVE_SKIP_PATHS (defaults to observer-sessions,.claude-mem; trims whitespace and skips empty patterns).
    • Updated observer-loop.sh to export ECC_SKIP_OBSERVE=1 and ECC_HOOK_PROFILE=minimal before calling claude, preventing the ECC→ECC self-observation loop.

Written for commit 5466281. Summary will update on new commits.

…p 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 (affaan-m#398)
Copilot AI review requested due to automatic review settings March 11, 2026 17:42
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds cooperative guards to the observer flow: observer-loop.sh exports ECC_SKIP_OBSERVE=1 and ECC_HOOK_PROFILE=minimal before spawning Claude/Haiku, and observe.sh gains multi-layer early-exit checks (entrypoint, profile, skip flag, agent_id, and path exclusions). No other control-flow or error-handling changes.

Changes

Cohort / File(s) Summary
Observer Loop Environment Guards
skills/continuous-learning-v2/agents/observer-loop.sh
Prefixes the Claude/Haiku invocation with ECC_SKIP_OBSERVE=1 and ECC_HOOK_PROFILE=minimal to prevent spawned observer sessions from triggering hooks.
Automated Session Detection Layers
skills/continuous-learning-v2/hooks/observe.sh
Adds multi-layer early exits: CLAUDE entrypoint check, ECC_HOOK_PROFILE=minimal, ECC_SKIP_OBSERVE env var, agent_id extraction from INPUT_JSON, and CWD-based exclusions via ECC_OBSERVE_SKIP_PATHS. Existing purge/observation logic is unchanged when guards do not match.

Sequence Diagram(s)

sequenceDiagram
  participant ObserverLoop as ObserverLoop (agent script)
  participant Claude as Claude (haiku)
  participant Hook as observe.sh
  participant FS as Filesystem/Env

  ObserverLoop->>FS: export ECC_SKIP_OBSERVE=1, ECC_HOOK_PROFILE=minimal
  ObserverLoop->>Claude: spawn Claude --model haiku (stdin prompt)
  Claude->>Hook: invoke hook with INPUT_JSON / stdin (tool call)
  Hook->>FS: read ECC_SKIP_OBSERVE / ECC_HOOK_PROFILE / STDIN_CWD
  alt any guard matches
    Hook-->>Claude: exit 0 (skip observation)
  else
    Hook->>FS: run observation/purge logic (record observations)
    Hook-->>Claude: finish processing
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰
I hopped through logs at dead of night,
Found loops that watched their own soft light.
I left a flag, a tiny seed—
Now Haiku sleeps; no extra feed.
Thump-thump, the garden breathes in peace.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a 5-layer automated session guard to observe.sh to prevent self-loop observations.
Linked Issues check ✅ Passed The PR implements all four required guard layers from issue #398: agent_id detection [1], ECC_HOOK_PROFILE=minimal [2], ECC_SKIP_OBSERVE [3], and CWD path exclusions [4], plus self-fix in observer-loop.sh.
Out of Scope Changes check ✅ Passed Changes are limited to the two files required: observe.sh (adding guard layers) and observer-loop.sh (self-fix), directly addressing issue #398 with no extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-signal filtering to prevent continuous-learning-v2’s observation hook from running during automated/programmatic Claude sessions, avoiding ECC self-observation loops and third-party tool noise.

Changes:

  • Add a 5-layer “automated session guard” block to hooks/observe.sh to early-exit for non-human sessions.
  • Update agents/observer-loop.sh to run Haiku analysis with ECC_SKIP_OBSERVE=1 and ECC_HOOK_PROFILE=minimal to break ECC→ECC recursion.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
skills/continuous-learning-v2/hooks/observe.sh Adds layered early-exit checks to skip observation in automated contexts (subagents/SDK entrypoints/minimal profile/explicit skip/path filters).
skills/continuous-learning-v2/agents/observer-loop.sh Sets env vars on the Haiku subprocess so the observer’s own analysis doesn’t generate new observations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

if [ -n "$STDIN_CWD" ]; then
IFS=',' read -ra _ECC_SKIP_ARRAY <<< "$_ECC_SKIP_PATHS"
for _pattern in "${_ECC_SKIP_ARRAY[@]}"; do
case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layer 5 path skipping can unintentionally disable all observations if ECC_OBSERVE_SKIP_PATHS contains an empty entry (e.g. trailing comma foo, or double comma foo,,bar). In that case _pattern becomes empty and the case "$STDIN_CWD" in *"$_pattern"*) branch matches every path. Consider trimming whitespace and skipping empty patterns before doing the match.

Suggested change
case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac
# Trim whitespace and skip empty patterns to avoid matching all paths
_trimmed_pattern="${_pattern//[[:space:]]/}"
[ -z "$_trimmed_pattern" ] && continue
case "$STDIN_CWD" in *"$_trimmed_pattern"*) exit 0 ;; esac

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +124
# Layer 1: Skip subagent sessions — agent_id is only present when a hook fires
# inside a subagent (automated by definition, never a human interactive session)
_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 2: CLAUDE_CODE_ENTRYPOINT — set by Claude Code itself to indicate how
# it was invoked. Non-interactive SDK/programmatic sessions use sdk-ts, sdk-py,
# sdk-cli, mcp, or remote. Interactive terminal sessions use "cli".
# This universally catches automation from ANY tool using the Anthropic SDK
# without requiring that tool to set any special env vars.
case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
sdk-ts|sdk-py|sdk-cli|mcp|remote) exit 0 ;;
esac

# Layer 3: Respect ECC_HOOK_PROFILE=minimal — suppresses non-essential hooks
[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard ordering does an extra JSON parse subprocess for agent_id before checking cheaper env-var based skips (CLAUDE_CODE_ENTRYPOINT, ECC_HOOK_PROFILE, ECC_SKIP_OBSERVE). To minimize overhead on automated runs (and match the “cheapest-first” intent), consider checking env vars first and/or extracting agent_id in the earlier Python parse that already reads the hook JSON.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
skills/continuous-learning-v2/hooks/observe.sh (1)

130-138: Consider trimming whitespace from user-supplied patterns (optional).

If a user configures ECC_OBSERVE_SKIP_PATHS="path1, path2" (space after comma), the second pattern becomes " path2" with a leading space, which may cause unexpected matching failures. The default value doesn't have this issue.

🔧 Optional fix to trim whitespace from patterns
 _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
+    _pattern="${_pattern%"${_pattern##*[![:space:]]}"}"  # trim trailing
     case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac
   done
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/hooks/observe.sh` around lines 130 - 138, The
skip-path matching can fail when ECC_OBSERVE_SKIP_PATHS contains spaces (e.g.,
"path1, path2") because _pattern will include leading/trailing whitespace;
update the splitting/loop that uses _ECC_SKIP_PATHS and _ECC_SKIP_ARRAY so each
_pattern is trimmed of leading and trailing whitespace before the case match
against STDIN_CWD (i.e., after reading into _ECC_SKIP_ARRAY, for each _pattern
perform a trim of whitespace into a new variable and use that trimmed variable
in the case check). Ensure you reference and trim the values of _pattern (from
_ECC_SKIP_ARRAY) derived from ECC_OBSERVE_SKIP_PATHS/_ECC_SKIP_PATHS so matching
is robust.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@skills/continuous-learning-v2/hooks/observe.sh`:
- Around line 130-138: The skip-path matching can fail when
ECC_OBSERVE_SKIP_PATHS contains spaces (e.g., "path1, path2") because _pattern
will include leading/trailing whitespace; update the splitting/loop that uses
_ECC_SKIP_PATHS and _ECC_SKIP_ARRAY so each _pattern is trimmed of leading and
trailing whitespace before the case match against STDIN_CWD (i.e., after reading
into _ECC_SKIP_ARRAY, for each _pattern perform a trim of whitespace into a new
variable and use that trimmed variable in the case check). Ensure you reference
and trim the values of _pattern (from _ECC_SKIP_ARRAY) derived from
ECC_OBSERVE_SKIP_PATHS/_ECC_SKIP_PATHS so matching is robust.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d3f987c-0d8b-4f08-a47a-26ac210dedbf

📥 Commits

Reviewing files that changed from the base of the PR and between da4db99 and ecd0be0.

📒 Files selected for processing (2)
  • skills/continuous-learning-v2/agents/observer-loop.sh
  • skills/continuous-learning-v2/hooks/observe.sh

…, fix empty pattern bug

Two issues flagged by Copilot and CodeRabbit in PR affaan-m#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`.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/continuous-learning-v2/hooks/observe.sh`:
- Around line 116-118: The current case on CLAUDE_CODE_ENTRYPOINT only lists a
finite set of non-cli values (sdk-ts|sdk-py|sdk-cli|mcp|remote) and will miss
any future non-`cli` entrypoints, allowing them to bypass the automation guard;
change the logic to explicitly allow only the `cli` entrypoint to proceed and
exit for any other value (i.e., treat anything not equal to "cli" as a guarded
case), updating the block that references CLAUDE_CODE_ENTRYPOINT and the
existing exit 0 use so new entrypoints automatically fall under the guard
without enumerating them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6389bf11-c495-4f81-bdd0-784791806a62

📥 Commits

Reviewing files that changed from the base of the PR and between ecd0be0 and e7df0e5.

📒 Files selected for processing (1)
  • skills/continuous-learning-v2/hooks/observe.sh

Comment thread skills/continuous-learning-v2/hooks/observe.sh
@ispaydeu

Copy link
Copy Markdown
Contributor Author

CI Status Note

The two failing checks are pre-existing failures on main unrelated to this PR:

  • Lint (markdownlint): Errors in skills/videodb/reference/api-reference.md, skills/videodb/reference/capture-reference.md, and skills/videodb/SKILL.md — introduced by the videodb PR merged before this one. Main branch run 22944395843 shows the same lint failure.

  • Windows tests: Also failing on main prior to this PR.

This PR only modifies two .sh files in skills/continuous-learning-v2/ — no markdown, no JS, no Node dependencies. All macOS and Linux test matrix entries pass.

@affaan-m

Copy link
Copy Markdown
Owner

Review outcome from local verification:

  • The PR direction is correct and it does close the main ECC self-observation loop in observer-loop.sh.
  • The cheaper-first ordering and skip-path trimming concerns have already been addressed in the current PR head.
  • One follow-up change is still advisable before merge in skills/continuous-learning-v2/hooks/observe.sh.

Remaining finding:

  1. CLAUDE_CODE_ENTRYPOINT currently uses a finite allowlist of known non-cli values (sdk-ts, sdk-py, sdk-cli, mcp, remote). That leaves a forward-compatibility hole: if Claude Code introduces another non-cli entrypoint later, it will fall through and be treated as interactive.

Safer shape:

case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
  cli) ;;
  *) exit 0 ;;
esac

That keeps the unset fallback interactive, but treats every explicit non-cli entrypoint as automated.

Local recommendation: needs one follow-up change before merge.

@affaan-m affaan-m merged commit c52a28a into affaan-m:main Mar 13, 2026
38 of 39 checks passed

@affaan-m affaan-m left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this retrospectively against merged head 5466281.

The core fix is directionally correct and it solved the primary problem: automated observer sessions no longer write observations, and observer-loop.sh now marks its Haiku subprocess with ECC_SKIP_OBSERVE=1 and ECC_HOOK_PROFILE=minimal.

Two follow-ups are still warranted:

  1. Project-registry side effects still happen before the guard exits.
    observe.sh extracts cwd and sources detect-project.sh before it reaches the new automated-session guard block. detect-project.sh eagerly creates homunculus project directories and updates projects.json, so automated sessions still leave metadata behind even though no observation is recorded. I reproduced this with CLAUDE_CODE_ENTRYPOINT=mcp, ECC_HOOK_PROFILE=minimal, ECC_SKIP_OBSERVE=1, agent_id, and skip-path payloads: each run exited 0, wrote 0 observations, but still created one project directory and one registry entry.

  2. The new guard matrix needs direct regression coverage.
    The existing hook suite still covers the legacy tool_output fallback path, but not the new guard branches for CLAUDE_CODE_ENTRYPOINT, ECC_HOOK_PROFILE=minimal, ECC_SKIP_OBSERVE, agent_id, or trimmed ECC_OBSERVE_SKIP_PATHS.

Suggested follow-up:

  • Move the automated-session guard block ahead of project detection, or add a no-side-effects project-detection path for skipped sessions.
  • Add explicit hook tests for each new guard branch so the loop-prevention behavior stays locked in.

Validation I ran on the merged PR head:

  • node tests/hooks/hooks.test.js -> 204 passed, 0 failed
  • Confirmed normal CLAUDE_CODE_ENTRYPOINT=cli still records observations.
  • Confirmed guarded paths suppress observation writes.

floatingman pushed a commit to floatingman/everything-claude-code that referenced this pull request Mar 22, 2026
…servations (affaan-m#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 (affaan-m#398)

* fix(observe): address review feedback — reorder guards cheapest-first, fix empty pattern bug

Two issues flagged by Copilot and CodeRabbit in PR affaan-m#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>
FrancescoRosciano pushed a commit to FRosciano-Mambo/everything-claude-code that referenced this pull request Jun 1, 2026
…servations (affaan-m#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 (affaan-m#398)

* fix(observe): address review feedback — reorder guards cheapest-first, fix empty pattern bug

Two issues flagged by Copilot and CodeRabbit in PR affaan-m#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

observe.sh fires on automated/observer sessions — no idle guard, no path exclusions, compounds with claude-mem creating double Haiku loops

3 participants