Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
50 changes: 23 additions & 27 deletions new_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
26 changes: 16 additions & 10 deletions scripts/test.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 ===")
Expand Down
Loading