Skip to content

Commit 940c3cd

Browse files
authored
Merge pull request #137 from Yif-Yang/fix/claude-tempdir-cleanup
fix(claude): use cleanup-tolerant temporary directory
2 parents 42270f7 + 6a193ff commit 940c3cd

2 files changed

Lines changed: 65 additions & 7 deletions

File tree

skillopt/model/claude_backend.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,11 +243,12 @@ def _assistant_message_schema_wrapper() -> str:
243243

244244
def _run_claude_print(*, system: str, prompt: str, model: str, tools: list[dict[str, Any]] | None, tool_choice: str | dict[str, Any] | None, return_message: bool, timeout: int | None, attachments: list[dict[str, Any]] | None = None) -> tuple[str, dict[str, Any], dict[str, int]]:
245245
effort = _normalize_reasoning_effort(REASONING_EFFORT)
246-
# Use mkdtemp + manual rmtree to avoid WinError 32 on Windows
247-
# where the spawned claude/node subprocess still holds a handle
248-
# to the temp directory when the context manager tries to clean up.
249-
temp_dir = tempfile.mkdtemp(prefix="skillopt_claude_")
250-
try:
246+
# A lingering claude/node handle can make Windows cleanup raise WinError 32.
247+
# Python 3.10+ supports suppressing cleanup failures while retaining
248+
# TemporaryDirectory's normal lifecycle and best-effort removal behavior.
249+
with tempfile.TemporaryDirectory(
250+
prefix="skillopt_claude_", ignore_cleanup_errors=True,
251+
) as temp_dir:
251252
copied_attachments = _copy_attachments_to_temp(attachments or [], temp_dir)
252253
prompt_for_cli = _append_attachment_instructions(prompt, copied_attachments)
253254
cmd = [CLAUDE_BIN, "-p", "--output-format", "json", "--permission-mode", CLAUDE_PERMISSION_MODE, "--add-dir", temp_dir]
@@ -291,8 +292,6 @@ def _run_claude_print(*, system: str, prompt: str, model: str, tools: list[dict[
291292
raw_text, result_event = _extract_result(stream)
292293
usage_info = _usage_from_result(result_event)
293294
return raw_text, result_event or {}, usage_info
294-
finally:
295-
shutil.rmtree(temp_dir, ignore_errors=True)
296295

297296

298297
def _compat_message_from_payload(payload: Any) -> CompatAssistantMessage:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Regression coverage for Claude CLI temporary-directory cleanup."""
2+
from __future__ import annotations
3+
4+
import json
5+
import os
6+
import shutil
7+
import tempfile
8+
from types import SimpleNamespace
9+
10+
from skillopt.model import claude_backend
11+
12+
13+
def test_claude_print_uses_cleanup_tolerant_temporary_directory(monkeypatch) -> None:
14+
cleanup_modes = []
15+
subprocess_cwd = ""
16+
17+
def simulated_windows_rmtree(cls, name, ignore_errors=False, repeated=False):
18+
del cls, repeated
19+
cleanup_modes.append(ignore_errors)
20+
if not ignore_errors:
21+
raise PermissionError(32, "directory is still in use", name)
22+
shutil.rmtree(name, ignore_errors=True)
23+
24+
def fake_run(*args, **kwargs):
25+
nonlocal subprocess_cwd
26+
subprocess_cwd = kwargs["cwd"]
27+
payload = {
28+
"type": "result",
29+
"result": "ok",
30+
"usage": {"input_tokens": 2, "output_tokens": 3},
31+
}
32+
return SimpleNamespace(
33+
returncode=0,
34+
stdout=json.dumps(payload) + "\n",
35+
stderr="",
36+
)
37+
38+
monkeypatch.setattr(
39+
tempfile.TemporaryDirectory,
40+
"_rmtree",
41+
classmethod(simulated_windows_rmtree),
42+
)
43+
monkeypatch.setattr(claude_backend.subprocess, "run", fake_run)
44+
45+
text, _, usage = claude_backend._run_claude_print(
46+
system="system",
47+
prompt="prompt",
48+
model="",
49+
tools=None,
50+
tool_choice=None,
51+
return_message=False,
52+
timeout=10,
53+
)
54+
55+
assert text == "ok"
56+
assert usage == {"prompt_tokens": 2, "completion_tokens": 3, "total_tokens": 5}
57+
assert cleanup_modes == [True]
58+
assert subprocess_cwd
59+
assert not os.path.exists(subprocess_cwd)

0 commit comments

Comments
 (0)