From b57ce15fff66cd9c77f146bc34cf658a8cbf0539 Mon Sep 17 00:00:00 2001 From: agent-admin Date: Fri, 24 Apr 2026 18:29:29 -0400 Subject: [PATCH] fix: prevent global hooks from firing inside SDK-spawned Claude subprocesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovered during Memoria Phase 4 first compile run: when compile.py invokes claude_agent_sdk.query(), the spawned `claude` subprocess inherits the global ~/.claude/settings.json hook config. Its SessionEnd hook then fires when the subprocess wraps up, triggering flush.py against today's daily log — polluting the log with compile metadata and creating a soft recursion (every compile call also generates a flush call). flush.py already had this guard (CLAUDE_INVOKED_BY=memory_flush set at module top before any SDK import). compile.py / query.py / lint.py did not. Add the same guard to the other three SDK call sites with script-specific sentinel values: - compile.py → memoria_compile - query.py → memoria_query - lint.py → memoria_lint The sentinel value doesn't matter — both session-end.py and pre-compact.py check `if os.environ.get("CLAUDE_INVOKED_BY"): exit(0)`, so any non-empty value short-circuits. Using distinct sentinels makes diagnostics clearer if a hook trace ever shows it. Verified: imports clean, all 29 acceptance tests still pass. --- scripts/compile.py | 10 +++++++++- scripts/lint.py | 8 +++++++- scripts/query.py | 8 +++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/compile.py b/scripts/compile.py index a0787e9..5ba9b3c 100644 --- a/scripts/compile.py +++ b/scripts/compile.py @@ -13,9 +13,17 @@ Usage: from __future__ import annotations +import os + +# Recursion guard — must be set BEFORE claude_agent_sdk imports anything that +# spawns a Claude subprocess. Inner SessionEnd / PreCompact hooks check +# CLAUDE_INVOKED_BY at startup and exit if set, preventing the global hooks +# from triggering flush.py against this process's daily log (which would +# pollute the log with compile metadata and could create a recursion loop). +os.environ["CLAUDE_INVOKED_BY"] = "memoria_compile" + import argparse import asyncio -import os import re import sys from pathlib import Path diff --git a/scripts/lint.py b/scripts/lint.py index 9b6e609..bfc2dc3 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -11,9 +11,15 @@ Usage: from __future__ import annotations +import os + +# Recursion guard — set before any SDK import so global SessionEnd/PreCompact +# hooks see CLAUDE_INVOKED_BY in the nested Claude subprocess's env and exit +# cleanly. See compile.py for full rationale. +os.environ["CLAUDE_INVOKED_BY"] = "memoria_lint" + import argparse import asyncio -import os from pathlib import Path from config import KNOWLEDGE_DIR, REPORTS_DIR, now_iso, today_iso diff --git a/scripts/query.py b/scripts/query.py index 9e87b54..e8c83ae 100644 --- a/scripts/query.py +++ b/scripts/query.py @@ -12,9 +12,15 @@ Usage: from __future__ import annotations +import os + +# Recursion guard — see compile.py for rationale. Set before any SDK import +# so the global SessionEnd/PreCompact hooks see CLAUDE_INVOKED_BY in the +# nested Claude subprocess's env and exit cleanly. +os.environ["CLAUDE_INVOKED_BY"] = "memoria_query" + import argparse import asyncio -import os from pathlib import Path from config import KNOWLEDGE_DIR, QA_DIR, now_iso