Skip to content

Commit f814bb2

Browse files
committed
fix: sanitize saved session ids
1 parent d44a980 commit f814bb2

4 files changed

Lines changed: 37 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ class HttpTool(Tool):
165165
quit Exit
166166
```
167167

168+
Saved session IDs are sanitized before they become filenames, so resume data stays inside `~/.corecoder/sessions`.
169+
168170
## How It Compares
169171

170172
| | Claude Code | Claw-Code | Aider | CoreCoder |

README_CN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ class HttpTool(Tool):
150150
quit 退出
151151
```
152152

153+
保存的会话 ID 会先安全化再作为文件名,恢复数据始终留在 `~/.corecoder/sessions` 目录内。
154+
153155
## 对比
154156

155157
| | Claude Code | Claw-Code | Aider | CoreCoder |

corecoder/session.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,36 @@
55
"""
66

77
import json
8-
import os
8+
import re
99
import time
1010
from pathlib import Path
1111

1212
SESSIONS_DIR = Path.home() / ".corecoder" / "sessions"
13+
_SAFE_SESSION_RE = re.compile(r"[^A-Za-z0-9._-]+")
14+
15+
16+
def _normalize_session_id(session_id: str | None) -> str:
17+
if not session_id:
18+
return f"session_{int(time.time())}"
19+
20+
name = session_id.strip().replace("\\", "/").split("/")[-1]
21+
name = _SAFE_SESSION_RE.sub("-", name).strip(".-_")
22+
return name or f"session_{int(time.time())}"
23+
24+
25+
def _session_path(session_id: str) -> Path:
26+
path = (SESSIONS_DIR / f"{_normalize_session_id(session_id)}.json").resolve()
27+
root = SESSIONS_DIR.resolve()
28+
if root != path.parent:
29+
raise ValueError("Invalid session id")
30+
return path
1331

1432

1533
def save_session(messages: list[dict], model: str, session_id: str | None = None) -> str:
1634
"""Save conversation to disk. Returns the session ID."""
1735
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
1836

19-
if not session_id:
20-
session_id = f"session_{int(time.time())}"
37+
session_id = _normalize_session_id(session_id)
2138

2239
data = {
2340
"id": session_id,
@@ -26,14 +43,14 @@ def save_session(messages: list[dict], model: str, session_id: str | None = None
2643
"messages": messages,
2744
}
2845

29-
path = SESSIONS_DIR / f"{session_id}.json"
46+
path = _session_path(session_id)
3047
path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
3148
return session_id
3249

3350

3451
def load_session(session_id: str) -> tuple[list[dict], str] | None:
3552
"""Load a saved session. Returns (messages, model) or None."""
36-
path = SESSIONS_DIR / f"{session_id}.json"
53+
path = _session_path(session_id)
3754
if not path.exists():
3855
return None
3956

tests/test_core.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ def test_session_save_load():
8989
pathlib.Path.home().joinpath(".corecoder/sessions/pytest_test_session.json").unlink()
9090

9191

92+
def test_session_name_is_sanitized():
93+
msgs = [{"role": "user", "content": "test message"}]
94+
sid = save_session(msgs, "test-model", "../Research Notes!")
95+
96+
assert sid == "Research-Notes"
97+
path = pathlib.Path.home().joinpath(".corecoder/sessions/Research-Notes.json")
98+
assert path.exists()
99+
assert load_session("../Research Notes!") is not None
100+
path.unlink()
101+
102+
92103
def test_session_not_found():
93104
assert load_session("nonexistent_session_id") is None
94105

0 commit comments

Comments
 (0)