claude-code-station 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ # SQLite スキーマ設計
2
+
3
+ > ccs v0.2.0 — 状態キャッシュDBのテーブル仕様
4
+
5
+ ## 配置
6
+
7
+ - パス: `~/.cache/ccs/state.db`
8
+ - XDG Base Directory 準拠(`$XDG_CACHE_HOME` があればそちら優先)
9
+ - 削除しても設定(`~/.config/ccs/repos.yml`)は無事、次回起動で再構築
10
+
11
+ ## 設計方針
12
+
13
+ - **Write-Once-per-Scan**: 走査1回につきトランザクション1つ、原子的に更新
14
+ - **Foreign Key**: 有効化(`PRAGMA foreign_keys = ON`)
15
+ - **WAL モード**: 並列読み書き向上(`PRAGMA journal_mode = WAL`)
16
+ - **マイグレーション**: `schema_version` テーブルで管理。後方互換壊す時は version++
17
+
18
+ ## テーブル一覧
19
+
20
+ | テーブル | 役割 | 行数イメージ |
21
+ |---|---|---|
22
+ | `schema_version` | DBマイグレーション管理 | 1 |
23
+ | `repos` | リポジトリ定義のキャッシュ | 〜50 |
24
+ | `repo_stats` | 走査で得た状態バッジ用データ | 〜50(repos と1:1) |
25
+ | `sessions` | Claude Code セッション要約キャッシュ | 〜5,000 |
26
+ | `handoff_files` | handoff/ 配下のファイル一覧(プレビュー用) | 〜500 |
27
+ | `pending_items` | pendings/ 配下のファイル一覧(プレビュー用) | 〜500 |
28
+ | `meta` | キー/値ストア(v2以降) | 〜10 |
29
+
30
+ ## DDL
31
+
32
+ ### schema_version
33
+
34
+ ```sql
35
+ CREATE TABLE schema_version (
36
+ version INTEGER PRIMARY KEY,
37
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
38
+ );
39
+ ```
40
+
41
+ ### repos
42
+
43
+ ```sql
44
+ CREATE TABLE repos (
45
+ name TEXT PRIMARY KEY, -- repos.yml の name
46
+ path TEXT NOT NULL,
47
+ description TEXT NOT NULL DEFAULT '',
48
+ command TEXT NOT NULL DEFAULT '', -- 空なら defaults/CCS_CMD
49
+ cwd TEXT, -- NULL なら path と同じ
50
+ tags_json TEXT NOT NULL DEFAULT '[]', -- JSON array
51
+ icon TEXT NOT NULL DEFAULT '📁',
52
+ disabled INTEGER NOT NULL DEFAULT 0, -- 0/1
53
+ scan_enabled INTEGER NOT NULL DEFAULT 1,
54
+ custom_json TEXT NOT NULL DEFAULT '{}', -- repos.yml の custom: をそのまま JSON 保存
55
+ config_hash TEXT NOT NULL, -- repos.yml の該当行のSHA256
56
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
57
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
58
+ );
59
+
60
+ CREATE INDEX idx_repos_path ON repos(path);
61
+ CREATE INDEX idx_repos_disabled ON repos(disabled);
62
+ ```
63
+
64
+ ### repo_stats
65
+
66
+ ```sql
67
+ CREATE TABLE repo_stats (
68
+ name TEXT PRIMARY KEY,
69
+ -- Git
70
+ is_git INTEGER NOT NULL DEFAULT 0,
71
+ branch TEXT,
72
+ last_commit_hash TEXT,
73
+ last_commit_subject TEXT,
74
+ last_commit_at TEXT, -- ISO 8601
75
+ uncommitted_files INTEGER NOT NULL DEFAULT 0,
76
+ uncommitted_insertions INTEGER NOT NULL DEFAULT 0,
77
+ uncommitted_deletions INTEGER NOT NULL DEFAULT 0,
78
+ -- Claude Code workspace
79
+ handoff_count INTEGER NOT NULL DEFAULT 0,
80
+ pending_count INTEGER NOT NULL DEFAULT 0,
81
+ claude_room_latest TEXT, -- 最新エントリのパス
82
+ claude_room_latest_at TEXT,
83
+ session_count_total INTEGER NOT NULL DEFAULT 0,
84
+ session_last_at TEXT, -- 最後のセッション日時
85
+ -- Scan metadata
86
+ scanned_at TEXT NOT NULL DEFAULT (datetime('now')),
87
+ scan_duration_ms INTEGER NOT NULL DEFAULT 0,
88
+ scan_error TEXT, -- エラー時のメッセージ
89
+ FOREIGN KEY (name) REFERENCES repos(name) ON DELETE CASCADE
90
+ );
91
+
92
+ CREATE INDEX idx_repo_stats_scanned_at ON repo_stats(scanned_at);
93
+ CREATE INDEX idx_repo_stats_session_last_at ON repo_stats(session_last_at DESC);
94
+ ```
95
+
96
+ ### sessions
97
+
98
+ ```sql
99
+ CREATE TABLE sessions (
100
+ uuid TEXT PRIMARY KEY,
101
+ repo_name TEXT, -- repos.name との紐付け(NULLable)
102
+ project_dir TEXT NOT NULL, -- ~/.claude/projects/<encoded>
103
+ cwd TEXT NOT NULL, -- セッションの作業ディレクトリ
104
+ branch TEXT,
105
+ started_at TEXT NOT NULL,
106
+ last_activity_at TEXT NOT NULL,
107
+ message_count INTEGER NOT NULL DEFAULT 0,
108
+ topic TEXT, -- 先頭user発言のダイジェスト(1行)
109
+ summary TEXT, -- <!-- ECC:SUMMARY --> あれば抽出
110
+ jsonl_size INTEGER NOT NULL DEFAULT 0,
111
+ jsonl_mtime TEXT NOT NULL, -- JSONLファイルの更新時刻(キャッシュ無効化用)
112
+ indexed_at TEXT NOT NULL DEFAULT (datetime('now')),
113
+ FOREIGN KEY (repo_name) REFERENCES repos(name) ON DELETE SET NULL
114
+ );
115
+
116
+ CREATE INDEX idx_sessions_repo ON sessions(repo_name, last_activity_at DESC);
117
+ CREATE INDEX idx_sessions_last_activity ON sessions(last_activity_at DESC);
118
+ CREATE INDEX idx_sessions_cwd ON sessions(cwd);
119
+ ```
120
+
121
+ ### handoff_files / pending_items
122
+
123
+ プレビューペインで先頭3件を素早く出すための補助テーブル。
124
+
125
+ ```sql
126
+ CREATE TABLE handoff_files (
127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
128
+ repo_name TEXT NOT NULL,
129
+ filename TEXT NOT NULL,
130
+ size INTEGER NOT NULL DEFAULT 0,
131
+ mtime TEXT NOT NULL,
132
+ first_line TEXT, -- プレビュー用に先頭100文字
133
+ FOREIGN KEY (repo_name) REFERENCES repos(name) ON DELETE CASCADE
134
+ );
135
+
136
+ CREATE INDEX idx_handoff_repo ON handoff_files(repo_name, mtime DESC);
137
+
138
+ CREATE TABLE pending_items (
139
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
140
+ repo_name TEXT NOT NULL,
141
+ filename TEXT NOT NULL,
142
+ size INTEGER NOT NULL DEFAULT 0,
143
+ mtime TEXT NOT NULL,
144
+ first_line TEXT,
145
+ FOREIGN KEY (repo_name) REFERENCES repos(name) ON DELETE CASCADE
146
+ );
147
+
148
+ CREATE INDEX idx_pending_repo ON pending_items(repo_name, mtime DESC);
149
+ ```
150
+
151
+ ### meta (v2以降)
152
+
153
+ キー/値ストア。スキャン時に解決した設定値(例: `defaults_command`)をDBに保存し、セッション一覧表示時の未マッピングセッションのコマンドフォールバックに利用。
154
+
155
+ ```sql
156
+ CREATE TABLE meta (
157
+ key TEXT PRIMARY KEY,
158
+ value TEXT NOT NULL
159
+ );
160
+ ```
161
+
162
+ **用途例:**
163
+ - `defaults_command`: 走査時に解決した最終フォールバック(`defaults.command` > `CCS_CMD` > `"claude"`)。ccs-list がセッション行生成時に、未マッピングセッション用の起動コマンドフォールバックに使用(レビューA-8)。
164
+
165
+ ## スキーマバージョン履歴
166
+
167
+ | Version | Applied | Changes |
168
+ |---------|---------|---------|
169
+ | 1 | v0.2.0 init | 初期テーブル群(repos, repo_stats, sessions, handoff_files, pending_items) |
170
+ | 2 | 2026-06-12 レビュークリーンアップ(v0.2.1) | meta テーブル追加(キー/値ストア、defaults_command 保存用) |
171
+
172
+ ## 走査フロー
173
+
174
+ > 順序重要(audit C-1): repo_stats のセッション集計(session_count_total /
175
+ > session_last_at)は sessions テーブルから再計算するため、**セッション走査が
176
+ > 先**。逆順だと集計が1スキャン分遅延する。
177
+
178
+ ```
179
+ ccs 起動
180
+ ├─ repos.yml 読み込み(ccs-config.ts: バリデーション + defaults解決)
181
+ ├─ repos テーブルと差分比較(config_hash) → 変更分だけ UPSERT
182
+ │ + meta.defaults_command に解決済みフォールバックを保存
183
+ ├─ ~/.claude/projects/ 走査(ccs-scan-sessions.ts)
184
+ │ JSONL mtime と sessions.jsonl_mtime 比較 → 変更分だけ再パース
185
+ │ 50MB超は既存行の size/mtime だけ更新(再パースloop防止)
186
+ │ cwd→repo 解決は lexical + realpath の最長プレフィックスマッチ
187
+ │ → sessions に UPSERT / 消滅分 DELETE / 未マッピング再解決
188
+ ├─ 全リポジトリを並列走査(Promise.allSettled, 同時実行上限=8)
189
+ │ 各リポジトリで:
190
+ │ - git rev-parse --is-inside-work-tree
191
+ │ - git branch --show-current
192
+ │ - git log -1 --format=%H%x00%s%x00%cI (NUL区切り)
193
+ │ - git diff --shortstat
194
+ │ - ls handoff/ pendings/ claude-room/
195
+ │ → repo_stats に UPSERT(sessions から集計を再計算)
196
+ └─ fzf 起動(DB読むだけ、高速)
197
+ ```
198
+
199
+ ## マイグレーション戦略
200
+
201
+ ```typescript
202
+ const migrations = [
203
+ { version: 1, up: `CREATE TABLE schema_version (...); ...初期DDL...` },
204
+ { version: 2, up: `CREATE TABLE meta (...);` },
205
+ // 以降 version: 3, 4, ... を追加
206
+ ];
207
+
208
+ function migrate(db: Database) {
209
+ const current = db.prepare('SELECT MAX(version) as v FROM schema_version').get()?.v ?? 0;
210
+ for (const m of migrations.filter(m => m.version > current)) {
211
+ db.transaction(() => {
212
+ db.exec(m.up);
213
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(m.version);
214
+ })();
215
+ }
216
+ }
217
+ ```
218
+
219
+ テーブル未存在時は `schema_version` 作成 → migration version 0 扱いで全マイグレーション実行。
220
+
221
+ ## キャッシュ無効化ルール
222
+
223
+ | データ | 無効化トリガ |
224
+ |---|---|
225
+ | `repos` | `repos.yml` のファイルmtime変更 |
226
+ | `repo_stats` | TTL 10秒 or ユーザー明示リフレッシュ(Ctrl-R) |
227
+ | `sessions` | JSONLファイルのmtime変更 |
228
+ | `handoff_files` / `pending_items` | ディレクトリmtime変更 |
229
+
230
+ `ccs --refresh` で全テーブル強制再走査。
231
+
232
+ ## ライブラリ選定
233
+
234
+ **better-sqlite3** を採用:
235
+ - 同期API(ccsのCLI用途にマッチ)
236
+ - プリペアドステートメント・トランザクション対応
237
+ - ネイティブバインディング(高速)
238
+ - macOS/Linux 両対応
239
+
240
+ 依存: `package.json` に `better-sqlite3 ^11.3.0` を追加。`install.sh --with-deps` で `npm install`(プロジェクトローカル)を実行、または `npm install` を手動で。グローバルインストール(`-g`)は推奨しない。
241
+
242
+ ## 想定サイズ
243
+
244
+ - DB本体: 1〜5MB(50リポジトリ、5,000セッションで試算)
245
+ - メモリ: 走査中でも 50MB 未満
246
+ - 初回走査時間: 10リポジトリで 〜2秒、50リポジトリで 〜5秒
247
+
248
+ ## セキュリティ
249
+
250
+ - `path` は `$HOME` 配下チェック(ccr v0.1.3 の既存ロジック維持)
251
+ - `first_line` にシークレットパターン検出時は `[REDACTED]` で置換
252
+ - SQL は全てプリペアドステートメント(better-sqlite3 の prepare API)
253
+ - DBファイル権限: `0600`(ユーザーのみ読み書き)
@@ -0,0 +1,40 @@
1
+ # v0.2.0 Regression Checklist (vs ccr v0.1.3)
2
+
3
+ Legend: ✅ verified by code review · ⚠️ needs manual test · ❌ regression detected
4
+
5
+ | # | Behavior | Status | Location (v0.2.0) |
6
+ |---|----------|--------|-------------------|
7
+ | 1 | UUID validation regex `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` | ✅ | `bin/ccs:220`, `bin/ccs-scan.ts:69`, `bin/ccs-preview-session.ts:15`/`71`, `bin/ccs-delete.sh:8` |
8
+ | 2 | `$HOME` path validation (reject/warn outside HOME) | ✅ | `bin/ccs-config.ts:128–132` (reject at load) + `bin/ccs:238–242` (fallback at launch) |
9
+ | 3 | 50 MB JSONL size cap | ✅ | `bin/ccs-scan.ts:64` (`MAX_JSONL_SIZE`) + `:554`; `bin/ccs-preview-session.ts:14`+`106` |
10
+ | 4 | Secret masking patterns (26 unified patterns: sk-ant, sk-*, ghp_, gho_, ghs_, ghu_, ghr_, github_pat_, glpat-, GOCSPX, SG., npm_, hooks.slack.com, AKIA, ASIA, AIza, stripe, Twilio, xox*, JWT, Bearer, op://, url-cred, env-assign, PEM) | ✅ | `bin/ccs-secrets.ts:44–90` (unified export `SECRET_PATTERNS[]`); both `bin/ccs-scan.ts` and `bin/ccs-preview-session.ts` import same `maskSecrets()` |
11
+ | 5 | pbcopy / xclip / xsel / wl-copy clipboard fallback chain | ✅ | `bin/ccs:21–34` |
12
+ | 6 | Adaptive header (≥80 cols full; <80 cols compact) | ✅ | `bin/ccs:172–177` |
13
+ | 7 | `ccs .` current-dir mode | ✅ | `bin/ccs:106` → `--current-only` flag → `bin/ccs-list.ts:277`/`288` |
14
+ | 8 | Ctrl-Y copy command | ✅ | `bin/ccs:142–148` |
15
+ | 9 | Ctrl-I copy ID / repo path | ✅ | `bin/ccs:150–156` |
16
+ | 10 | Ctrl-D delete (resume only, ignored on new) | ✅ | `bin/ccs:160–166`; `bin/ccs-delete.sh` (UUID check at :10) |
17
+ | 11 | Ctrl-R refresh | ✅ | `bin/ccs:169` |
18
+ | 12 | No `eval`; no shell expansion of user-controlled strings | ⚠️ | See review notes — fzf `--bind` strings embed `${CLIP_CMD}` / `${CCS_LIST}` / `${CCS_DELETE}` / `${LIST_ARGS_STR}` unquoted. `CLIP_CMD` is internally generated; others are script-controlled absolute paths. Pass-through `{4}`, `{5}`, `{6}` fzf placeholders run inside a double-quoted subshell string. No user config values reach `bash` without quoting **except** via `ROW_CMD` expansion in the final execution section (intentional, see #13). |
19
+ | 13 | Direct command invocation (NOT `exec`) so shell functions / aliases like `opr` work | ✅ | `bin/ccs:256–266` — `${ROW_CMD}` is word-split on purpose. Documented at lines 254–256. |
20
+ | 14 | `CCR_CMD` → `CCS_CMD` migration warning | ✅ | `bin/ccs:14–18` (bash-level) + `bin/ccs-config.ts:205–218` (TS-level, once per process) |
21
+ | 15 | Missing dependency detection (fzf / tsx / node / claude / tput) | ✅ | `bin/ccs:37–56`. **Note:** tput added in v0.2.0 (was optional in ccr). |
22
+ | 16 | `--help` / `-h` flag | ✅ | `bin/ccs:111` |
23
+ | 17 | `--version` / `-v` flag | ✅ | `bin/ccs:112` |
24
+ | 18 | Risky-command warning (shell metacharacters rejection) | ✅ | Phase 6 S2で旧 rm/sudo/`;` 警告は削除。現在はSHELL_METACHARS拒否(`;&\|<>$\`"'\\`+制御文字)で正式ガード。コード参照: `bin/ccs-sanitize.ts:22` (SHELL_METACHARS定義) + `bin/ccs-config.ts:164–175` (検証実装) |
25
+ | 19 | Repo name shell-metachar rejection | ✅ | `bin/ccs:226–229` (`;&|<>$\``) |
26
+ | 20 | Pre-scan auto-run (with `--no-scan` / `--refresh` overrides) | ✅ | `bin/ccs:120–128` |
27
+ | 21 | Pass-through flags to `claude` (e.g., `--dangerously-skip-permissions`) | ✅ | `bin/ccs:113` + `:260`/`:264` (`PASS_ARGS[@]`) |
28
+ | 22 | Cancel via Esc / Ctrl-C | ✅ | `bin/ccs:195` (`|| exit 0`) |
29
+ | 23 | Session file delete also cleans subagents dir | ✅ | `bin/ccs-delete.sh:43–47` |
30
+ | 24 | Preview pane secret-masking (full 11-pattern list) | ✅ | `bin/ccs-preview-session.ts:18–38` |
31
+ | 25 | First-run scaffold (`repos.yml` + `README.md`, mode 0600) | ✅ | `bin/ccs-config.ts:138–148` |
32
+ | 26 | XDG_CONFIG_HOME / XDG_CACHE_HOME honored | ✅ | `bin/ccs-config.ts:106–119` |
33
+ | 27 | Pass-through of unknown CLI options (last-resort fallback) | ⚠️ | `bin/ccs:113` pushes unknown args into `PASS_ARGS`. Unlike ccr which may have rejected unknowns — verify this is intentional. |
34
+
35
+ ## Items requiring manual verification
36
+
37
+ - (#4) Run against a repo containing an `AIza*` token in handoff/ — v0.2.0 scan will NOT redact it (only preview will).
38
+ - (#6) Resize terminal below 80 cols and verify header changes.
39
+ - (#12) Exercise Ctrl-Y/I/D on selections with names containing quotes/spaces/UTF-8.
40
+ - (#27) Confirm whether `ccs --unknown-flag` should error or pass through.
@@ -0,0 +1,151 @@
1
+ # v0.2.0 Static Review Notes
2
+
3
+ Severity per `~/.claude/rules/common/code-review.md`:
4
+ CRITICAL (block) · HIGH (warn) · MEDIUM (info) · LOW (note)
5
+
6
+ ---
7
+
8
+ ## HIGH
9
+
10
+ ### H1. Inconsistent secret-masking patterns between scan and preview ✅ FIXED 2026-04-14
11
+ > Extracted shared patterns into `bin/ccs-secrets.ts` (13 unified patterns, single `[REDACTED]` sentinel). Both `ccs-scan.ts` and `ccs-preview-session.ts` now import from it; inline pattern arrays removed.
12
+
13
+ - **Where:** `bin/ccs-scan.ts:74–80` (5 patterns) vs `bin/ccs-preview-session.ts:18–30` (11 patterns).
14
+ - **Issue:** The scan engine writes `handoff_files.first_line` / `pending_items.first_line` into the SQLite DB after masking with only 5 patterns. Preview pane (which does not read those DB columns) uses 11. Tokens like `sk-ant-*`, `gho_*`, `ghs_*`, `op://*`, `AIza*` will land in `state.db` in cleartext.
15
+ - **Impact:** Secret leak into on-disk cache (mode 0600, but still an audit surface and a potential grep target).
16
+ - **Fix:** Extract one shared `SECRET_PATTERNS` constant (e.g., `bin/ccs-secrets.ts`) and import from both. Unify the replacement sentinel (`[REDACTED]` vs `sk-***`).
17
+
18
+ ### H2. fzf preview invokes shell-injection-prone `exec` via bash heredoc ✅ FIXED 2026-04-14
19
+ > `bin/ccs-config.ts` now rejects any `path`/`cwd` containing shell metacharacters (`;&|<>$\`"'\\`, control chars) after tilde-expansion, throwing `ConfigError`. Test added in `test/ccs-config.test.ts`.
20
+
21
+ - **Where:** `bin/ccs:160–166` (`ctrl-d` bind), `bin/ccs:142–156` (`ctrl-y`/`ctrl-i` binds).
22
+ - **Issue:** `${CLIP_CMD}` (auto-derived) and `${CCS_DELETE}` / `${CCS_LIST}` / `${LIST_ARGS_STR}` (script-controlled absolute paths) are interpolated into the `execute(...)` string unquoted at bash substitution time. Then fzf expands `{4}` / `{5}` inside bash double-quotes at row selection time.
23
+ - Row columns `{4}` (KIND:KEY) / `{5}` (CWD) come from DB rows that originated in `repos.yml`. A repo `path:` containing e.g. `"; malicious-cmd; :"` would, if such characters made it past config validation, be evaluated by the inner `sh -c` that fzf spawns.
24
+ - Current config validation bans `\t\n\\` in names and validates paths via `resolve()` + `isUnderHome()`, but **does not explicitly reject shell metachars in the `path` string itself**. A symlink-safe path like `~/dir";echo OWNED #"` would satisfy all current checks.
25
+ - **Impact:** Potential shell-injection on keybinding activation if an attacker can write `repos.yml` (same threat as a malicious `~/.bashrc`, but worth hardening given v0.2.0's AI-agent-editable config story).
26
+ - **Fix (pick one):**
27
+ 1. In `bin/ccs-config.ts`, reject any `path` / `cwd` containing `` ;&|<>$`"' `` (matching what `bin/ccs:226` already does for `name` in a different context).
28
+ 2. Pre-validate `{5}` inside the fzf `execute` block before `printf`.
29
+
30
+ ### H3. Race on concurrent `ccs` invocations
31
+ - **Where:** `bin/ccs-scan.ts:805–820` + `bin/ccs-db.ts:189–216`.
32
+ - **Issue:** WAL mode is enabled (good), but two simultaneous `ccs` runs both call `scanSessions()` which DELETEs rows where `uuid NOT IN (...)`. If process A sees an in-flight file that process B has not yet stat'd, B may stale-delete A's freshly-indexed row. No advisory lock file.
33
+ - **Impact:** Transient "session disappears from list" if the user opens two terminals and both auto-scan. Self-heals on next scan.
34
+ - **Fix:** Add an `flock`-style lock file at `<cacheDir>/scan.lock` in `openDb()` or `scan()`, or switch the cleanup to `scanned_at < started_at - grace`.
35
+
36
+ ---
37
+
38
+ ## MEDIUM
39
+
40
+ ### M1. `ccs-parse.ts` appears obsolete in v0.2.0 ✅ FIXED 2026-04-14
41
+ > Deleted `bin/ccs-parse.ts`. Verified no remaining references in `bin/`, `install.sh`, or docs (only this changelog entry).
42
+
43
+ - **Where:** `bin/ccs-parse.ts` — not referenced by `bin/ccs`, `bin/ccs-list.ts`, `bin/ccs-scan.ts`, or `bin/ccs-preview.ts`.
44
+ - **Fix:** Either delete, or add a banner comment marking it deprecated/legacy.
45
+
46
+ ### M2. Module-level `migrationWarned` state leaks across test runs
47
+ - **Where:** `bin/ccs-config.ts:205`.
48
+ - **Issue:** Dynamic `import()` caches by URL; successive `loadConfig()` calls in the same process share the flag. Harmless in production (warning already fired once), but means the deprecation test must run in isolation (first) or use a child process. Current tests use dynamic import which does NOT bust the cache for `.ts` files through `tsx`'s loader.
49
+ - **Fix:** Export a `resetMigrationWarning()` test hook, or inject the env lookup.
50
+
51
+ ### M3. No input sanitization of `scan_error` before DB write
52
+ - **Where:** `bin/ccs-scan.ts:384–387`.
53
+ - **Issue:** `err.message` from `git` / filesystem calls flows into `repo_stats.scan_error` unfiltered. Low risk (preview renders it via console.log, not HTML), but a malicious repo name containing ANSI escapes could pollute fzf preview rendering.
54
+ - **Fix:** Strip ESC (`\x1b`) before storing.
55
+
56
+ ### M4. `better-sqlite3` opens DB synchronously on every subprocess ✅ FIXED 2026-04-14
57
+ > `openDb()` now accepts `{ skipMigrate, readonly }`. `bin/ccs-preview.ts` opens with `{ readonly: true, skipMigrate: true }` — no migration round-trip, no write-pragma cost on hot preview path.
58
+
59
+ - **Where:** `bin/ccs-preview.ts:193` runs `openDb()` for every preview invocation (every arrow-key press in fzf).
60
+ - **Issue:** At 100+ repos/sessions, each preview opens/closes a SQLite connection (~5–15ms). Noticeable lag on key-repeat. Schema migrations re-check on every open (idempotent but still a round-trip).
61
+ - **Fix:** Skip `migrate()` in preview code path (add `{ skipMigrate: true }` option to `openDb`), or use `readonly: true`.
62
+
63
+ ### M5. `bin/ccs-list.ts:225` label concat produces double-icon artifact ✅ FIXED 2026-04-14
64
+ > Replaced the string-replace hack with a conditional `iconPart` expression. Visual output unchanged (`🔄 Name` for unmapped / 🔄-iconned, `🔄 📁 Name` for repos with icons).
65
+
66
+ ```ts
67
+ const label = `${icon === "🔄" ? "🔄" : "🔄 " + icon} ${displayName}`.replace("🔄 🔄", "🔄");
68
+ ```
69
+ - Relies on a string-replace hack to normalize. Easier: `const label = \`🔄 ${icon && icon !== "🔄" ? icon + " " : ""}${displayName}\`;`.
70
+
71
+ ### M6. SQL injection risk — verified ZERO
72
+ - All `better-sqlite3` calls use `.prepare()` + parameter binding or `@name` maps. The only dynamic SQL fragment is `ccs-db.ts:277` (`IN (?,?,...)`), built with `?` placeholders — safe.
73
+
74
+ ---
75
+
76
+ ## LOW
77
+
78
+ ### L1. Cross-platform paths
79
+ - `bin/ccs-scan.ts:638` hardcodes `join(homedir(), ".claude", "projects")`. Fine on macOS/Linux. Windows isn't targeted (bash-only launcher), so not a blocker.
80
+
81
+ ### L2. Error message style inconsistency
82
+ - `[ccs]` vs `[ccs-scan]` vs `[ccs-db]` vs `[ccs-list]` prefixes. OK, but not uniform with `❌` / `⚠️` emoji rules seen in `bin/ccs` itself.
83
+
84
+ ### L3. `ccs-list.ts` — no `humanTime()` for sessions (uses `formatSessionStamp`) while repos use `humanTime()`. Intentional? Creates visual asymmetry in the list.
85
+
86
+ ### L4. `ccs-preview.ts:73` relies on local TZ in `Date().getHours()` — list and preview may disagree on "session at 23:59" if session crosses midnight vs a user in a different TZ.
87
+
88
+ ### L5. `bin/ccs-config.ts` `ensureConfigDir` writes template files with `mode: 0o600` but `writeFileSync` only applies mode on create — this is correct. Worth a comment so future editors don't add a chmod.
89
+
90
+ ### L6. No `.gitignore` for `state.db`-wal/-shm sibling files. Harmless since the cache dir is outside the repo.
91
+
92
+ ---
93
+
94
+ ## Performance at 100+ repos
95
+
96
+ - `scanAllRepos` uses a semaphore-bounded `Promise.allSettled` with `parallelism=8`. Good.
97
+ - `scanOneRepo` spawns 4 git subprocesses per repo (≈ 400 processes for 100 repos). Each has a 5 s timeout. Cold-cache scan of 100 large repos could take tens of seconds. Consider batching via a single `git -C ... && git -C ...` piped call.
98
+ - `ccs-list.ts` uses `LEFT JOIN repo_stats` — fine with current indexes.
99
+
100
+ ---
101
+
102
+ ## Ship recommendation
103
+
104
+ **YELLOW** — H1 (secret-pattern drift) and H2 (path-metachar validation gap) should be fixed before v0.2.0 ships to prevent credential leakage into `state.db` and to close the shell-injection hole on keybinds. H3 (scan race) is acceptable to defer. Tests are written and should pass after `npm install` once those two HIGH issues are fixed.
105
+
106
+ ---
107
+
108
+ ## Phase 6 fixes (CR2) — 2026-04-14
109
+
110
+ Second-round review by 4 specialist agents (typescript / security / database / general). Found 0 CRITICAL, 8 HIGH (2 ship-blockers), 13 MEDIUM. Fixed 16 items in Phase 6; 5 MEDIUM + LOW items deferred to v0.2.1 (see `docs/v0.2.1-backlog.md`).
111
+
112
+ ### Security
113
+
114
+ - **S1 (HIGH, ship blocker) ✅ FIXED 2026-04-14** — `bin/ccs-config.ts:resolveRepoEntry` now runs the `SHELL_METACHARS` regex against the resolved `command` string (including defaults/env fallback) and throws `ConfigError("command for repo at index N contains shell metacharacter(s): ...")`. Closes the last injection vector since `bin/ccs` invokes `${ROW_CMD}` unquoted.
115
+ - **S2 (HIGH, ship blocker) ✅ FIXED 2026-04-14** — `bin/ccs:160–170` parses the fzf TSV selection via `IFS=$'\t' read -r _ _ _ KIND_KEY ROW_CWD ROW_CMD <<< "$SELECTED"`; the old informational `rm/sudo/;` warning block was removed because metachar rejection at config time (S1) is now the authoritative guard.
116
+ - **TAB coverage (MEDIUM) ✅ VERIFIED 2026-04-14** — `bin/ccs-config.ts:SHELL_METACHARS` uses `\x00-\x1f` which covers TAB (0x09), NL (0x0a), CR (0x0d), and all other C0 control chars; explanatory comment added. `command` now goes through the same regex.
117
+ - **Custom size cap (MEDIUM) ✅ FIXED 2026-04-14** — `bin/ccs-config.ts:resolveRepoEntry` serializes `raw.custom` and rejects anything over 64,000 JSON bytes before cloning; error message includes byte count.
118
+ - **Secret pattern coverage (MEDIUM) ✅ FIXED 2026-04-14** — `bin/ccs-secrets.ts:SECRET_PATTERNS` expanded from 13 → 19: added `aws-sts` (ASIA session creds), `stripe-live` / `stripe-test` (sk_/rk_), `twilio-account` (AC + 32 hex), `jwt` (3-segment base64url), `db-url` (postgres/mysql/mongodb/redis URIs with embedded creds, incl. `+srv`). Inline comment block documents each pattern's origin.
119
+ - **Unmasked session topic/summary (MEDIUM) ✅ FIXED 2026-04-14** — `bin/ccs-scan.ts:parseSessionJsonl` now applies `maskSecrets()` to both `topic` (cap 200 chars) and `summary` (cap 1000 chars) **before** truncation, so a secret straddling the cap boundary is still redacted intact.
120
+
121
+ ### TypeScript
122
+
123
+ - **H2 CR2 (HIGH, ccs-scan CLI bootstrap) ✅ FIXED 2026-04-14** — `bin/ccs-scan.ts:848–858` now passes an onRejected handler to `main().then(...)`. Synchronous throws before `main()`'s try block (e.g., `loadConfig`'s `ConfigError`) are caught, logged as `[ccs-scan] fatal: ...`, and exit with code 1 instead of silently exiting 0.
124
+ - **H1 CR2 (HIGH, M4 regression in ccs-list.ts) ✅ FIXED 2026-04-14** — `bin/ccs-list.ts:main()` called `openDb(paths.stateDb)` without options, re-opening the DB in write mode and re-running `migrate()` on every fzf list refresh. Replaced with `openDb(paths.stateDb, { readonly: true, skipMigrate: true })`; `finally` block now calls `close()` instead of `handle.close()`. Hot path is now read-only and migration-free.
125
+ - **H3 CR2 (HIGH, blocking I/O in ccs-preview-session.ts) ✅ FIXED 2026-04-14** — `bin/ccs-preview-session.ts` imported `readdirSync` / `readFileSync` / `statSync` from `fs`, blocking the event loop on every preview render. Swapped for `readdir` / `readFile` / `stat` from `node:fs/promises`; `renderSessionPreview` is now `async (...): Promise<void>`; all three call sites awaited. Zero remaining `*Sync(` calls.
126
+ - **H4 CR2 (HIGH, type safety in ccs-preview-session.ts CLI) ✅ FIXED 2026-04-14** — `bin/ccs-preview-session.ts` CLI bootstrap passed `process.argv[2]` (typed `string | undefined`) directly to `renderSessionPreview(sessionId: string)`. Added early-return guard; `process.exit`'s `never` return narrows `sessionId` to `string` naturally. Promise rejection is now caught via `.catch(err => { ...; process.exit(1); })` so CLI exit code propagates correctly.
127
+ - **HOME fallback (MEDIUM) ✅ FIXED 2026-04-14** — `bin/ccs-list.ts:sessionToRow` used `s.cwd.replace(process.env.HOME || "", "~")`, which garbles output when `HOME` is unset (replaces every empty-string match). Switched to `homedir()` from `node:os` (falls back to `getpwuid_r` on POSIX / `USERPROFILE` on Windows).
128
+ - **`node:` prefix on builtins (MEDIUM) ✅ FIXED 2026-04-14** — `bin/ccs-db.ts` now imports `mkdirSync, chmodSync, existsSync` from `node:fs` and `dirname` from `node:path` (was bare `"fs"` / `"path"`). No other bare builtin imports present.
129
+
130
+ ### Database
131
+
132
+ - **H5 (HIGH, SQLite 999-variable limit in ccs-db.ts) ✅ FIXED 2026-04-14** — `bin/ccs-db.ts:deleteReposNotIn` rewrote the dynamic `IN (?, ?, ...)` pattern (which caps at SQLite's 999-variable bind limit, ~500 repos after overhead) to `name NOT IN (SELECT value FROM json_each(?))` with `JSON.stringify(names)` as the single bound argument. JSON1 is default-enabled in better-sqlite3's bundled SQLite (≥3.38). Empty-array semantic preserved (still `DELETE FROM repos`).
133
+
134
+ ### General
135
+
136
+ - **Stale header in ccs-delete.sh (MEDIUM) ✅ FIXED 2026-04-14** — `bin/ccs-delete.sh:2` header comment updated from `# ccr-delete.sh - ...` to `# ccs-delete.sh - ...`. No other `ccr` references in the script.
137
+ - **Dependency note in sqlite-schema.md (MEDIUM) ✅ FIXED 2026-04-14** — `docs/design/sqlite-schema.md:210` rewritten to state `package.json` carries `better-sqlite3 ^11.3.0` with `install.sh --with-deps` running a local `npm install`; explicitly discourages `-g` global install (which was the previous inaccurate guidance).
138
+
139
+ ---
140
+
141
+ ## Phase 7 fixes (CR3) — 2026-04-14
142
+
143
+ ### Security
144
+ - **A** (HIGH): `ccs-list.ts:248` — moved `openDb()` into try block with cold-start friendly error message ✅ FIXED
145
+ - **B** (MEDIUM): `ccs-scan.ts:318` — apply `maskSecrets` to `last_commit_subject` before DB write ✅ FIXED
146
+ - **C** (MEDIUM): `ccs-scan.ts:368-370` — apply `maskSecrets` to `scan_error` (DB + stderr) ✅ FIXED
147
+
148
+ ### Quality / Docs
149
+ - **D** (MEDIUM): `ccs-db.ts:241` — `close()` runs `wal_checkpoint(PASSIVE)` (write mode only) ✅ FIXED
150
+ - **E** (MEDIUM): `ccs-preview-session.ts:8-9` — `"path"` / `"os"` → `node:` prefix ✅ FIXED
151
+ - **F** (MEDIUM): `docs/design/repos-yml-schema.md` — added command metachar validation row ✅ FIXED