diff --git a/TODO.md b/TODO.md index 101ca9d..e2f2f5e 100644 --- a/TODO.md +++ b/TODO.md @@ -96,7 +96,8 @@ directory before launching the interactive session bypasses the dialog. - [x] T001: Add `ensure_workspace_trusted()` to new_session.py — pre-creates projects dir. Also fixed `get_project_logs_dir` slug encoding to match Claude Code (regex `[^a-zA-Z0-9-]` instead of only replacing `\/:.`) - [x] T002: Add tests for ensure_workspace_trusted and fixed slug encoding (68 total, merged in PR #15) -- [ ] T003: Fix pretrust — mkdir alone doesn't work, use `claude -p` to create real trust state +- [x] T003: Fix pretrust — mkdir alone doesn't work, use `claude -p` to create real trust state (didn't work either) +- [ ] T004: Fix pretrust v2 — trust is stored in `~/.claude.json` projects[path].hasTrustDialogAccepted, write it directly ## Rename: context-reset → new-session (007) diff --git a/new_session.py b/new_session.py index fd2de50..6275d7d 100644 --- a/new_session.py +++ b/new_session.py @@ -959,39 +959,35 @@ def get_project_logs_dir(project_dir): def ensure_workspace_trusted(project_dir): - """Run a minimal `claude -p` session to establish workspace trust. + """Write trust state to ~/.claude.json so the trust dialog is skipped. Claude Code shows "Is this a project you trust?" on first interactive launch - in a new directory. The -p (print) flag skips the trust dialog and creates - the proper trust state (project dir + session JSONL). Subsequent interactive - launches in the same directory won't prompt. + in a new directory. Trust state is stored in ~/.claude.json under + projects[path].hasTrustDialogAccepted. Writing this flag directly skips + the dialog instantly with no subprocess or API call. - No-ops if the project already has a session file. + No-ops if the project is already trusted. """ - logs_dir = get_project_logs_dir(project_dir) - if os.path.exists(logs_dir): - # Check for at least one JSONL session file (dir alone isn't enough) - jsonls = [f for f in os.listdir(logs_dir) if f.endswith('.jsonl')] - if jsonls: - return # Already trusted with a real session + config_path = os.path.join(os.path.expanduser("~"), ".claude.json") + # Normalize to forward slashes — Claude Code uses this format on Windows + project_key = os.path.abspath(project_dir).replace("\\", "/") try: - log(f"Pre-trusting workspace via claude -p in {project_dir}") - cmd = ['claude', '-p', 'ok', '--dangerously-skip-permissions'] - result = subprocess.run( - cmd, cwd=project_dir, timeout=30, - capture_output=True, text=True, - startupinfo=_si(), - ) - if result.returncode == 0: - log("Pre-trust complete") - else: - log(f"WARNING: pre-trust claude -p returned {result.returncode}") - except subprocess.TimeoutExpired: - log("WARNING: pre-trust claude -p timed out after 30s") - except FileNotFoundError: - log("WARNING: claude binary not found, cannot pre-trust") + config = {} + if os.path.exists(config_path): + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + projects = config.setdefault("projects", {}) + entry = projects.setdefault(project_key, {}) + if entry.get("hasTrustDialogAccepted"): + return # Already trusted + entry["hasTrustDialogAccepted"] = True + entry.setdefault("allowedTools", []) + entry.setdefault("hasCompletedProjectOnboarding", True) + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + log(f"Pre-trusted workspace in ~/.claude.json: {project_key}") except Exception as e: - log(f"WARNING: pre-trust failed: {e}") + log(f"WARNING: could not pre-trust workspace: {e}") def get_newest_jsonl(logs_dir): diff --git a/scripts/test.py b/scripts/test.py index 8b37375..dca476f 100644 --- a/scripts/test.py +++ b/scripts/test.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Tests for new_session.py -- run with: python scripts/test.py""" +import json import os import sys import subprocess @@ -220,16 +221,21 @@ def test(name, condition): with tempfile.TemporaryDirectory() as d: fake_proj = os.path.join(d, "fake-project") os.makedirs(fake_proj) - logs_dir = context_reset.get_project_logs_dir(fake_proj) - test("dir does not exist before trust", not os.path.exists(logs_dir)) - # Simulate already-trusted: create dir + JSONL, verify no-op - os.makedirs(logs_dir, exist_ok=True) - seed = os.path.join(logs_dir, "00000000-0000-0000-0000-000000000000.jsonl") - with open(seed, "w") as fh: - fh.write("{}\n") - # Should be a no-op (already has JSONL) — no subprocess spawned - context_reset.ensure_workspace_trusted(fake_proj) - test("skips when JSONL exists (no-op)", os.path.exists(seed)) + # Temporarily point ensure_workspace_trusted at a temp config file + fake_config = os.path.join(d, ".claude.json") + import unittest.mock + with unittest.mock.patch('new_session.os.path.expanduser', return_value=d): + context_reset.ensure_workspace_trusted(fake_proj) + # Verify trust was written + with open(fake_config, 'r') as fh: + config = json.load(fh) + proj_key = os.path.abspath(fake_proj).replace("\\", "/") + entry = config.get("projects", {}).get(proj_key, {}) + test("hasTrustDialogAccepted is True", entry.get("hasTrustDialogAccepted") is True) + test("has allowedTools", "allowedTools" in entry) + # Second call is a no-op + context_reset.ensure_workspace_trusted(fake_proj) + test("idempotent (no error on second call)", True) # --- get_newest_jsonl --- print("\n=== get_newest_jsonl ===")