From bfa53bc46d2741b7d942118de801b7d400e927b4 Mon Sep 17 00:00:00 2001 From: carpedkm Date: Sat, 20 Jun 2026 13:28:34 +0000 Subject: [PATCH 1/2] fix(sleep): make --bare conditional on ANTHROPIC_API_KEY (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClaudeCliBackend._call() and attempt_with_tools() hardcoded --bare, which skips Claude CLI's credential resolution. This broke subscription- token auth: every model call silently returned "Not logged in" and scored 0 — the user saw "baseline 0.0 → candidate 0.0, gate reject" with no indication of an auth failure. Fix: only pass --bare when ANTHROPIC_API_KEY is set. The remaining isolation flags (--disable-slash-commands, --disallowedTools, --exclude-dynamic-system-prompt-sections, clean temp cwd) already provide the needed isolation without --bare. Also adds _detect_cli_error() to log a warning when CLI output matches known auth error patterns, so auth failures surface loudly instead of deflating every score to 0. Co-Authored-By: Claude Fable 5 --- skillopt_sleep/backend.py | 47 ++++++++++++++++++++++++----- tests/test_sleep_engine.py | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/skillopt_sleep/backend.py b/skillopt_sleep/backend.py index eeb0a1b..3640fdd 100644 --- a/skillopt_sleep/backend.py +++ b/skillopt_sleep/backend.py @@ -553,6 +553,31 @@ class ClaudeCliBackend(CliBackend): timeout=timeout) self.claude_path = claude_path + # Known CLI error prefixes that indicate auth or config failures. + # When detected, we log a warning so the user doesn't mistake a + # broken auth for "nothing to optimize" (issue #68). + _CLI_ERROR_MARKERS = ( + "Not logged in", + "Please run /login", + "Authentication required", + "API key", + "Unauthorized", + "Invalid API", + ) + + def _detect_cli_error(self, stdout: str, stderr: str) -> None: + """Log a warning if CLI output looks like an auth/config error.""" + import logging + combined = stdout + "\n" + stderr + for marker in self._CLI_ERROR_MARKERS: + if marker in combined: + logging.getLogger("skillopt_sleep").warning( + "Claude CLI returned a likely auth error: %s", + combined[:200].replace("\n", " "), + ) + self.last_call_error = combined[:500] + return + def _call(self, prompt: str, *, max_tokens: int = 1024) -> str: # Run ISOLATED so the ambient Claude Code environment does not leak into # the optimizer/target call. Critically, the user's GLOBAL skills @@ -560,14 +585,17 @@ class ClaudeCliBackend(CliBackend): # them explicitly — without this, reflect/attempt sometimes reply with a # list of the user's installed skills instead of doing the task. # --bare skip hooks, LSP, plugins (minimal mode) + # Only safe with ANTHROPIC_API_KEY auth; + # breaks subscription-token auth (#68). # --disable-slash-commands disable all skills # --disallowedTools '*' no tool use # --exclude-dynamic-... drop per-machine cwd/env/memory/git sections # cwd= no project CLAUDE.md import tempfile - cmd = [ - self.claude_path, "-p", "--output-format", "text", - "--bare", + cmd = [self.claude_path, "-p", "--output-format", "text"] + if os.environ.get("ANTHROPIC_API_KEY"): + cmd.append("--bare") + cmd += [ "--disable-slash-commands", "--disallowedTools", "*", "--exclude-dynamic-system-prompt-sections", @@ -588,7 +616,9 @@ class ClaudeCliBackend(CliBackend): shutil.rmtree(clean_cwd, ignore_errors=True) except Exception: pass - return (proc.stdout or "").strip() + out = (proc.stdout or "").strip() + self._detect_cli_error(out, proc.stderr or "") + return out def attempt_with_tools(self, task, skill, memory, tools): # Expose a REAL, callable `search` tool (a shell shim that logs each @@ -625,9 +655,11 @@ class ClaudeCliBackend(CliBackend): f"# Task\n{task.intent}\n\n{task.context_excerpt}\n\n" "Return ONLY the final answer text." ) - cmd = [ - self.claude_path, "-p", "--output-format", "text", - "--bare", "--disable-slash-commands", + cmd = [self.claude_path, "-p", "--output-format", "text"] + if os.environ.get("ANTHROPIC_API_KEY"): + cmd.append("--bare") + cmd += [ + "--disable-slash-commands", "--allowedTools", "Bash", "--exclude-dynamic-system-prompt-sections", ] @@ -639,6 +671,7 @@ class ClaudeCliBackend(CliBackend): cmd, capture_output=True, text=True, timeout=self.timeout, cwd=work, ) resp = (proc.stdout or "").strip() + self._detect_cli_error(resp, proc.stderr or "") except Exception: resp = "" self._tokens += len(prompt) // 4 + len(resp) // 4 diff --git a/tests/test_sleep_engine.py b/tests/test_sleep_engine.py index de74dae..4e4bc8b 100644 --- a/tests/test_sleep_engine.py +++ b/tests/test_sleep_engine.py @@ -944,5 +944,67 @@ class TestCopilotBackend(unittest.TestCase): shutil.rmtree(stub_dir, ignore_errors=True) +class TestClaudeCliBackendBare(unittest.TestCase): + """Issue #68: --bare must be conditional on ANTHROPIC_API_KEY.""" + + def test_bare_included_when_api_key_set(self): + """With ANTHROPIC_API_KEY, --bare should appear in the command.""" + from skillopt_sleep.backend import ClaudeCliBackend + be = ClaudeCliBackend(claude_path="/usr/bin/false", timeout=5) + with unittest.mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test"}): + # We can't run the real CLI, but we can inspect cmd construction + # by monkeypatching subprocess.run to capture the command. + captured = {} + def fake_run(cmd, **kwargs): + captured["cmd"] = cmd + class FakeProc: + stdout = "hello" + stderr = "" + returncode = 0 + return FakeProc() + with unittest.mock.patch("subprocess.run", side_effect=fake_run): + be._call("test prompt") + self.assertIn("--bare", captured["cmd"]) + + def test_bare_omitted_without_api_key(self): + """Without ANTHROPIC_API_KEY, --bare should NOT appear.""" + from skillopt_sleep.backend import ClaudeCliBackend + be = ClaudeCliBackend(claude_path="/usr/bin/false", timeout=5) + env = os.environ.copy() + env.pop("ANTHROPIC_API_KEY", None) + with unittest.mock.patch.dict(os.environ, env, clear=True): + captured = {} + def fake_run(cmd, **kwargs): + captured["cmd"] = cmd + class FakeProc: + stdout = "hello" + stderr = "" + returncode = 0 + return FakeProc() + with unittest.mock.patch("subprocess.run", side_effect=fake_run): + be._call("test prompt") + self.assertNotIn("--bare", captured["cmd"]) + + def test_cli_error_detected_and_logged(self): + """Auth errors in CLI output should trigger a warning.""" + from skillopt_sleep.backend import ClaudeCliBackend + be = ClaudeCliBackend(claude_path="/usr/bin/false", timeout=5) + captured = {} + def fake_run(cmd, **kwargs): + captured["cmd"] = cmd + class FakeProc: + stdout = "Not logged in · Please run /login" + stderr = "" + returncode = 0 + return FakeProc() + with unittest.mock.patch.dict(os.environ, {}, clear=False): + with unittest.mock.patch("subprocess.run", side_effect=fake_run): + result = be._call("test prompt") + # The error string is returned as output (backwards-compat) + self.assertIn("Not logged in", result) + # But it's also recorded for detection + self.assertIn("Not logged in", getattr(be, "last_call_error", "")) + + if __name__ == "__main__": unittest.main(verbosity=2) From 552ddefd74f2a6fdf57d4cd40681f2be5236fb34 Mon Sep 17 00:00:00 2001 From: carpedkm Date: Sat, 20 Jun 2026 13:32:43 +0000 Subject: [PATCH 2/2] fix: narrow CLI error markers to avoid false positives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address codex review: "API key" was too generic — a model response about configuring API keys would trigger a false auth warning. Now: - Use specific phrases ("Invalid API key", "Unauthorized: invalid x-api-key") - Only check short stdout (<300 chars) to skip real model responses - Still check stderr unconditionally Co-Authored-By: Claude Fable 5 --- skillopt_sleep/backend.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/skillopt_sleep/backend.py b/skillopt_sleep/backend.py index 3640fdd..f472da7 100644 --- a/skillopt_sleep/backend.py +++ b/skillopt_sleep/backend.py @@ -556,19 +556,26 @@ class ClaudeCliBackend(CliBackend): # Known CLI error prefixes that indicate auth or config failures. # When detected, we log a warning so the user doesn't mistake a # broken auth for "nothing to optimize" (issue #68). + # Keep these specific to avoid false positives on normal model output. _CLI_ERROR_MARKERS = ( "Not logged in", "Please run /login", "Authentication required", - "API key", - "Unauthorized", - "Invalid API", + "Invalid API key", + "Unauthorized: invalid x-api-key", ) def _detect_cli_error(self, stdout: str, stderr: str) -> None: - """Log a warning if CLI output looks like an auth/config error.""" + """Log a warning if CLI output looks like an auth/config error. + + Only checks stderr and short stdout (< 300 chars) to avoid + false-positives on legitimate model responses that mention + auth-related terms. + """ import logging - combined = stdout + "\n" + stderr + # Long stdout is almost certainly a real model response, not an error. + check_stdout = stdout if len(stdout) < 300 else "" + combined = check_stdout + "\n" + stderr for marker in self._CLI_ERROR_MARKERS: if marker in combined: logging.getLogger("skillopt_sleep").warning(