6.3 KiB
| title | aliases | tags | sources | created | updated | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| macOS Python Version — System Python 3.9 vs Homebrew in Claude Code Hooks |
|
|
|
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 | Noneunion syntax requires Python 3.10+ — using|for type unions (PEP 604) fails withSyntaxErroron Python 3.9- Hooks silently fail: Claude Code hooks execute the command in a subprocess; if the Python script raises a
SyntaxErroron startup, the hook exits non-zero and the status message disappears — no visible error to the user - Fix: replace all
python3in~/.claude/settings.jsonhook commands with/opt/homebrew/bin/python3 - After the fix,
xlsxwriteror 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:
- SessionEnd hook fires
/usr/bin/python3 hook.pyis called- Python 3.9 raises
SyntaxErroron line 3 - Hook exits with code 1
- Status message disappears
- Nothing is written to
daily/YYYY-MM-DD.md - 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.
Related Concepts
- wiki/concepts/memory-compiler-mac-migration — Mac migration context where this issue was discovered
- wiki/claude-code/hooks — Claude Code hook system including SessionStart, PreCompact, SessionEnd
- wiki/concepts/remote-server-dotfiles-bootstrap — server setup scripts where Python version awareness is also important
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 usePath | Nonesyntax (requires 3.10+); fix was replacing allpython3with/opt/homebrew/bin/python3in~/.claude/settings.json;xlsxwriteralso installed for Homebrew Python