Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "remember",
"description": "Continuous memory for Claude Code. Extracts, summarizes, and compresses conversations into tiered daily logs. Claude remembers what you did yesterday.",
"version": "0.8.2",
"version": "0.8.3",
"author": {
"name": "Digital Process Tools"
},
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.3] — Windows: resolve the claude.cmd shim before spawning

### Fixed

- **Every auto-save silently failed on Windows** ([#120](https://github.com/Digital-Process-Tools/claude-remember/issues/120)) — `pipeline/haiku.py` spawned the CLI in list-form as `subprocess.run(["claude", ...])`. The npm global install ships the CLI only as a `claude.cmd` shim (no `claude.exe`), and Python's `subprocess` goes through `CreateProcess`, which resolves only `.exe` from a bare name — so every spawn raised `FileNotFoundError: [WinError 2]`. The pipeline aborted right after `[haiku] calling`, so `now.md` / `today-*.md` / `recent.md` were never generated (the SessionStart hook and `/remember` skill kept working since they don't spawn `claude`). The binary is now resolved with `shutil.which("claude")`, which honours `PATHEXT` and returns the full `claude.cmd` path that `subprocess` launches fine — no `shell=True`, no argv-length regression, and cross-platform safe (returns the plain path on Linux/macOS). Override via `REMEMBER_CLAUDE_BIN`. Reported with a precise diagnosis and tested patch by the issue author.

## [0.8.2] — Oversized-extract guard keeps long sessions saving

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
[![OS](https://img.shields.io/badge/tested%20on-Linux%20%7C%20macOS%20%7C%20Windows-blue)](https://github.com/Digital-Process-Tools/claude-remember/actions/workflows/tests.yml)
[![License](https://img.shields.io/badge/license-Community-brightgreen)](LICENSE)
[![Version](https://img.shields.io/badge/version-0.8.2-orange)](.claude-plugin/plugin.json)
[![Version](https://img.shields.io/badge/version-0.8.3-orange)](.claude-plugin/plugin.json)

Claude Code starts every session blank. It doesn't know what you worked on yesterday, what conventions your team follows, or what mistakes it already made. You re-explain everything, every time.

Expand Down
24 changes: 23 additions & 1 deletion pipeline/haiku.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import json
import os
import re
import shutil
import subprocess
import tempfile

Expand Down Expand Up @@ -71,6 +72,27 @@ def _resolve_model() -> str:
return raw if raw else DEFAULT_MODEL


def _resolve_claude_bin() -> str:
"""Full path to the ``claude`` executable, resolved before spawning.

On Windows the npm global install ships the CLI only as a ``claude.cmd``
shim (no ``claude.exe``). ``subprocess`` goes through ``CreateProcess``,
which only resolves ``.exe`` from a bare name — so ``["claude", ...]`` dies
with ``FileNotFoundError: [WinError 2]`` and silently kills every auto-save
(#120). ``shutil.which`` honours ``PATHEXT`` and returns the full
``claude.cmd`` path, which ``subprocess`` launches fine (no ``shell=True``,
no argv-length regression); on Linux/macOS it returns the plain path.

REMEMBER_CLAUDE_BIN overrides the lookup (mirrors REMEMBER_MODEL /
REMEMBER_MAX_TURNS). When ``which`` finds nothing, fall back to the bare
name so behaviour matches the pre-fix code on a misconfigured PATH.
"""
override = os.environ.get("REMEMBER_CLAUDE_BIN", "").strip()
if override:
return override
return shutil.which("claude") or "claude"


def _child_env() -> dict[str, str]:
"""Environment for the nested ``claude -p`` with the PARENT session vars
stripped.
Expand Down Expand Up @@ -119,7 +141,7 @@ def call_haiku(
# list too long") at exec time and silently kills saves of long sessions.
# `claude -p` with no positional prompt reads the prompt from stdin.
cmd = [
"claude",
_resolve_claude_bin(),
"-p",
"--output-format", "json",
"--no-session-persistence",
Expand Down
57 changes: 56 additions & 1 deletion tests/test_haiku.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def test_call_haiku_success(mock_run, monkeypatch):

args = mock_run.call_args
cmd = args[0][0]
assert "claude" in cmd
assert os.path.basename(cmd[0]).startswith("claude")
assert "--model" in cmd
assert "haiku" in cmd
# One-shot summarization subprocess: never resume these, never write to disk
Expand Down Expand Up @@ -430,3 +430,58 @@ def test_reject_gate_custom_pattern_applies(monkeypatch):
monkeypatch.setenv("REMEMBER_REJECT_PATTERN", r"^banana")
assert _parse_response(_mock_claude_response("banana split")).is_skip is True
assert _parse_response(_mock_claude_response("I cannot do that.")).is_skip is False


# --- REMEMBER_CLAUDE_BIN: resolve the claude.cmd shim on Windows (#120) -------
from pipeline.haiku import _resolve_claude_bin


def test_resolve_claude_bin_uses_which(monkeypatch):
"""Default resolves the full path via shutil.which (queried for "claude")."""
monkeypatch.delenv("REMEMBER_CLAUDE_BIN", raising=False)
with patch("pipeline.haiku.shutil.which", return_value="/usr/local/bin/claude") as w:
assert _resolve_claude_bin() == "/usr/local/bin/claude"
w.assert_called_once_with("claude")


def test_resolve_claude_bin_windows_cmd_shim(monkeypatch):
"""shutil.which honours PATHEXT and returns the full claude.cmd path, which
subprocess CAN launch — a bare "claude" cannot (CreateProcess only resolves
.exe from a bare name), which is what kills every auto-save on Windows (#120)."""
monkeypatch.delenv("REMEMBER_CLAUDE_BIN", raising=False)
shim = r"C:\Users\x\AppData\Roaming\npm\claude.cmd"
with patch("pipeline.haiku.shutil.which", return_value=shim):
assert _resolve_claude_bin() == shim


def test_resolve_claude_bin_env_override(monkeypatch):
"""REMEMBER_CLAUDE_BIN wins over which (mirrors REMEMBER_MODEL / _MAX_TURNS)."""
monkeypatch.setenv("REMEMBER_CLAUDE_BIN", "/opt/claude/bin/claude")
with patch("pipeline.haiku.shutil.which", return_value="/usr/local/bin/claude"):
assert _resolve_claude_bin() == "/opt/claude/bin/claude"


def test_resolve_claude_bin_blank_override_falls_back(monkeypatch):
monkeypatch.setenv("REMEMBER_CLAUDE_BIN", " ")
with patch("pipeline.haiku.shutil.which", return_value="/usr/local/bin/claude"):
assert _resolve_claude_bin() == "/usr/local/bin/claude"


def test_resolve_claude_bin_not_on_path_falls_back(monkeypatch):
"""which finds nothing → fall back to the bare name, preserving the prior
behaviour on a misconfigured PATH instead of returning None / crashing."""
monkeypatch.delenv("REMEMBER_CLAUDE_BIN", raising=False)
with patch("pipeline.haiku.shutil.which", return_value=None):
assert _resolve_claude_bin() == "claude"


@patch("pipeline.haiku.subprocess.run")
def test_call_haiku_uses_resolved_bin(mock_run, monkeypatch):
"""cmd[0] must be the RESOLVED binary path, not the bare "claude" — else
Windows' CreateProcess raises WinError 2 on the claude.cmd shim (#120)."""
monkeypatch.delenv("REMEMBER_CLAUDE_BIN", raising=False)
mock_run.return_value = MagicMock(
returncode=0, stdout=_mock_claude_response("x"), stderr="")
with patch("pipeline.haiku.shutil.which", return_value="/usr/local/bin/claude"):
call_haiku("p")
assert mock_run.call_args[0][0][0] == "/usr/local/bin/claude"
Loading