obsidian/wiki/concepts/macos-python-version-hooks.md
2026-04-26 21:10:46 +01:00

6.3 KiB

title aliases tags sources created updated
macOS Python Version — System Python 3.9 vs Homebrew in Claude Code Hooks
macos-python-hooks
homebrew-python-hooks
python-version-hooks
python-union-type-syntax
python
macos
claude-code
hooks
homebrew
migration
daily/2026-04-22.md
2026-04-22 2026-04-22

macOS Python Version — System Python 3.9 vs Homebrew in Claude Code Hooks

macOS ships with a system Python at /usr/bin/python3 that is locked to Python 3.9. Homebrew-installed Python (typically at /opt/homebrew/bin/python3) can be Python 3.12, 3.13, or 3.14. Claude Code hooks that use Python 3.10+ syntax (such as the X | Y union type notation for type hints) will silently exit with an error when called by python3 if the system Python is resolved. The fix is to use the explicit Homebrew Python path in all hook commands.

Key Points

  • macOS system /usr/bin/python3 = Python 3.9 on modern Macs — this is a locked Apple-provided stub, not the full CPython distribution
  • Path | None union syntax requires Python 3.10+ — using | for type unions (PEP 604) fails with SyntaxError on Python 3.9
  • Hooks silently fail: Claude Code hooks execute the command in a subprocess; if the Python script raises a SyntaxError on startup, the hook exits non-zero and the status message disappears — no visible error to the user
  • Fix: replace all python3 in ~/.claude/settings.json hook commands with /opt/homebrew/bin/python3
  • After the fix, xlsxwriter or other third-party packages must also be installed for the Homebrew Python: /opt/homebrew/bin/pip3 install <package>

Details

Why the System Python Is Stuck at 3.9

Apple ships a Python shim at /usr/bin/python3 that redirects to an Xcode Command Line Tools Python. This Python version is controlled by Apple's release cycle and is not updated via system updates. On macOS Sequoia (15.x) and Sonoma (14.x), this stub typically resolves to Python 3.9.x. Homebrew installs its own CPython build at /opt/homebrew/bin/python3 that can be any version managed by brew upgrade.

The discrepancy becomes critical when scripts use syntax introduced in Python 3.10 or later:

# PEP 604: union types with | operator — requires Python 3.10+
from pathlib import Path

def process(file: Path | None) -> str | None:
    ...

# Python 3.9 equivalent (works on both):
from typing import Optional
def process(file: Optional[Path]) -> Optional[str]:
    ...

A hook script with Path | None syntax imports cleanly under Homebrew Python 3.14 but raises SyntaxError: unsupported operand type(s) for | under system Python 3.9.

The Silent Failure Mode

Claude Code hooks show a status message while running (e.g., "Saving session to knowledge base..."). If the hook exits with a non-zero code, the status message disappears. The session appears to end normally. No error is displayed to the user in the chat interface.

This makes Python version issues particularly hard to diagnose:

  1. SessionEnd hook fires
  2. /usr/bin/python3 hook.py is called
  3. Python 3.9 raises SyntaxError on line 3
  4. Hook exits with code 1
  5. Status message disappears
  6. Nothing is written to daily/YYYY-MM-DD.md
  7. User notices days later that no sessions have been logged

The Fix: Explicit Homebrew Python Path

In ~/.claude/settings.json, replace every occurrence of python3 (bare command) with /opt/homebrew/bin/python3:

{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [{
          "type": "command",
          "command": "CLAUDE_INVOKED_BY=memory_flush /opt/homebrew/bin/python3 ~/.claude/memory-compiler/hooks/session-end.py",
          "statusMessage": "Saving session to knowledge base...",
          "timeout": 10
        }]
      }
    ],
    "SessionStart": [
      {
        "hooks": [{
          "type": "command",
          "command": "/opt/homebrew/bin/python3 ~/.claude/obsidian-session-start.py ...",
          "statusMessage": "Loading Obsidian context..."
        }]
      }
    ]
  }
}

Installing Packages for Homebrew Python

After switching to Homebrew Python, packages installed via system pip may not be available. Install them explicitly for the Homebrew Python:

/opt/homebrew/bin/pip3 install xlsxwriter
/opt/homebrew/bin/pip3 install python-dotenv
# Or via uv (recommended for the memory-compiler)
uv run --directory ~/.claude/memory-compiler python -c "import xlsxwriter"

For the memory-compiler project specifically, uv manages its own virtualenv and is not affected by the system vs Homebrew distinction — uv run always uses the venv Python. Only direct python3 invocations in hook commands need the explicit path.

Verifying the Python Version

# Check which python3 is in PATH (may differ between shell and subprocess)
which python3
python3 --version

# Check the Homebrew python3 explicitly
/opt/homebrew/bin/python3 --version

# Test that the hook script's syntax is valid
/opt/homebrew/bin/python3 -c "import py_compile; py_compile.compile('~/.claude/memory-compiler/hooks/session-end.py')"

Context: obsidian-session-log Without transcript_path

When obsidian-session-log is run without a transcript_path (e.g., manually triggered from the terminal rather than by the SessionEnd hook), it writes only "session ended" to the daily log without an AI-generated summary. This is expected behavior — the AI summary requires the JSONL transcript file that Claude Code provides to hooks via stdin. Manual runs without that context produce a minimal log entry. This is not a bug; it's a limitation of running outside the hook context.

Sources

  • daily/2026-04-22.md — Mac migration to ai_leed@192.168.1.44: stop-hooks silently completed with no output; root cause was /usr/bin/python3 = 3.9 on new Mac, hook scripts use Path | None syntax (requires 3.10+); fix was replacing all python3 with /opt/homebrew/bin/python3 in ~/.claude/settings.json; xlsxwriter also installed for Homebrew Python