Plugin hooks: shared Python process caches sys.modules across tool calls, causing stale import errors

Resolved 💬 3 comments Opened Feb 4, 2026 by kitaekatt Closed Mar 11, 2026

Summary

Plugin hooks experience stale Python module state that persists after the underlying code/paths have been corrected. Errors resolve spontaneously after several minutes without any code or config changes. This suggests Claude Code's hook execution environment caches Python module lookups across tool calls within a session.

I don't have a minimal reproduction case. I've been trying to isolate one for ~2 weeks without success — the behavior is intermittent and tied to module caching state that's difficult to control externally. I'm filing this anyway because the Claude Code team likely has knowledge of the internal hook execution architecture that would make reproducing this straightforward.

Environment

  • Claude Code CLI 2.1.29 (latest as of 2026-02-04)
  • Ubuntu Linux
  • Multiple plugins enabled via enabledPlugins in settings.json
  • Plugins sourced from a local marketplace directory

Observed behavior

1. Import errors persist after source fix

A hook imported from lib.yaml_config_cache import .... The module existed at the correct path in the plugin's python/lib/ directory. A direct Python test confirmed the import worked:

python3 -c "import sys; sys.path.insert(0, '<plugin-cache-path>/python'); from lib.yaml_config_cache import load_resource_gating_config; print('OK')"
# OK

Yet inside Claude Code, the hook continued to fail with No module named 'lib.yaml_config_cache' for ~5-10 minutes, then resolved spontaneously. No code or config changes were made between the last failure and first success.

2. Cross-plugin namespace collision via shared sys.modules

We identified a concrete trigger: two plugins both had a lib Python package at different paths:

  • context-explorer/scripts/lib/ (transcript, context_types, display modules)
  • session-lifecycle-kit/python/lib/ (yaml_config_cache, resource_matcher modules)

When Claude Code runs hooks from both plugins, whichever lib package gets imported first wins in sys.modules. The other plugin's from lib.X import ... then fails because it finds the wrong lib package.

Key evidence: Error profiles alternated between No module named 'yaml_config_cache' and No module named 'file_metadata' across consecutive tool calls within the same session — consistent with shared sys.modules state where load order varies.

We fixed this by renaming one plugin's lib to slk_lib, but the fix required a cache refresh + restart to take effect — the in-process module cache persisted across the source fix.

3. UnboundLocalError: cannot access local variable 'json'

Five identical errors appear on every tool call:

unknown (unknown): cannot access local variable 'json' where it is not associated with a value

The source is listed as unknown — the hook crashes before it can identify itself. No plugin hook file contains a json = ... variable assignment. The error may originate in Claude Code's own hook error-handling code (e.g., a json import shadowed by a local variable in an exception path). These errors also appear and disappear spontaneously across tool calls.

4. Errors shift without intervention

Within a single session, without any user changes:

  • Errors appear → persist through config changes → disappear spontaneously
  • New, different errors appear from different hooks → then also disappear
  • The pattern is consistent with cached state expiring over time

What we've tried

  • Restarted Claude Code — errors reappear in the new session
  • Deleted the entire plugin cache directory (~/.claude/plugins/cache/), restarted Claude Code, let it regenerate — same errors
  • Deleted all .pyc files and __pycache__ directories in the plugin cache
  • Verified correct file contents in the regenerated cache via grep
  • Confirmed imports work in fresh Python processes outside Claude Code
  • Switched from local marketplace to --plugin-dir — same errors
  • Converted local marketplace to a GitHub-hosted marketplace — same errors
  • None of these actions resolved the errors — they resolved on their own after time elapsed

Hypothesis

Claude Code's hook execution environment runs plugin hooks in a shared Python process (or reuses sys.modules state across hook invocations within a session). This means:

  1. Cross-plugin namespace collisions: If two plugins have a package with the same name (e.g., lib), the first to import wins in sys.modules and all subsequent imports from other plugins resolve to the wrong package
  2. Stale module state: After fixing source code and refreshing the cache, the in-process module state retains the old (broken) imports until some internal TTL or cache invalidation occurs
  3. Non-deterministic load order: Which plugin's hook runs first varies across tool calls, causing errors to shift between different modules

This would explain why:

  • Direct Python tests always work (fresh process, no shared state)
  • Hooks fail inside Claude Code (shared sys.modules across plugins)
  • Errors resolve spontaneously (internal cache expires)
  • Deleting .pyc files and the plugin cache doesn't help (the cache is in the running process, not on disk)
  • Error profiles alternate between different modules (load order varies)

Suggested fix

If hooks from different plugins share a Python process, each plugin's hook invocation should either:

  1. Get its own isolated sys.modules / sys.path scope
  2. Or run in a fresh subprocess per invocation
  3. Or at minimum, clean up sys.path and relevant sys.modules entries after each hook completes

---

<sub>Previously filed as #22223, which was auto-closed as a duplicate of #13568, #15621, and #18517. None are duplicates:

  • #13568: Plugin's own incorrect sys.path logic (wrong parent directory added) — our imports work correctly in standalone Python
  • #15621: Old plugin version directories persist on disk after update — our cache has the correct single version, the staleness is in-process memory
  • #18517: CLAUDE_PLUGIN_ROOT not re-expanded in settings.json after version change — our settings.json contains no expanded plugin paths</sub>

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗