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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Launch a new Claude Code session in any project. Context reset (same project) or
Main file: `new_session.py`. No dependencies beyond Python stdlib.
`context_reset.py` is a backward-compat alias that re-exports everything from `new_session.py`.

- **Pre-trust**: Checks `~/.claude.json` for workspace trust. Claude Code walks parent directories, so a trusted parent (e.g. `~/Documents/ProjectsCL1`) covers all children. Only writes a new entry if no ancestor is trusted.
- **Phase 1**: Launch new terminal tab with `claude '<prompt>'`
- **Phase 1b**: Wait for new Claude process (process count check, 15s timeout)
- **Phase 2**: Verify new session is active (transcript file growth, configurable timeout)
Expand Down Expand Up @@ -34,6 +35,6 @@ The prompt tells the new session to read SESSION_STATE.md (transcript context) a
## Testing

```bash
python scripts/test.py # 62 tests
python scripts/test.py # 70 tests
python new_session.py --project-dir . --dry-run # verify command without executing
```
5 changes: 4 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ 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)
- [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
- [x] T004: Fix pretrust v2 — trust is stored in `~/.claude.json` projects[path].hasTrustDialogAccepted, write it directly (PR #17 merged)
- [x] T005: Verified — pretrust-test tab launched WITHOUT trust dialog. Feature works end-to-end.
- [x] T006: Parent trust walk — check parent dirs before writing per-project entries, update CLAUDE.md docs (70 tests)
- [ ] T007: Add `--stop` flag to new_session.py — kills current tab without launching a new one

## Rename: context-reset → new-session (007)

Expand Down
30 changes: 20 additions & 10 deletions new_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,10 +963,10 @@ def ensure_workspace_trusted(project_dir):

Claude Code shows "Is this a project you trust?" on first interactive launch
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.
projects[path].hasTrustDialogAccepted. Claude Code walks parent directories
when checking trust, so a trusted parent covers all children.

No-ops if the project is already trusted.
No-ops if the project or any parent directory is already trusted.
"""
config_path = os.path.join(os.path.expanduser("~"), ".claude.json")
# Normalize to forward slashes — Claude Code uses this format on Windows
Expand All @@ -976,13 +976,23 @@ def ensure_workspace_trusted(project_dir):
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)
projects = config.get("projects", {})
# Check if this exact path or any parent is already trusted
check = project_key
while True:
entry = projects.get(check, {})
if entry.get("hasTrustDialogAccepted"):
return # Already trusted (exact match or parent)
parent = check.rsplit("/", 1)[0] if "/" in check else ""
if not parent or parent == check:
break
check = parent
# Not trusted — write entry for this project
projects_mut = config.setdefault("projects", {})
new_entry = projects_mut.setdefault(project_key, {})
new_entry["hasTrustDialogAccepted"] = True
new_entry.setdefault("allowedTools", [])
new_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}")
Expand Down
19 changes: 19 additions & 0 deletions scripts/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,25 @@ def test(name, condition):
context_reset.ensure_workspace_trusted(fake_proj)
test("idempotent (no error on second call)", True)

# Parent trust walk: trust parent, child should be skipped
child_proj = os.path.join(fake_proj, "sub", "deep")
os.makedirs(child_proj)
# fake_proj is already trusted — child should inherit
context_reset.ensure_workspace_trusted(child_proj)
with open(fake_config, 'r') as fh:
config2 = json.load(fh)
child_key = os.path.abspath(child_proj).replace("\\", "/")
test("parent trust skips child write", child_key not in config2.get("projects", {}))

# Untrusted path (outside fake_proj) should get written
other_proj = os.path.join(d, "other-project")
os.makedirs(other_proj)
context_reset.ensure_workspace_trusted(other_proj)
with open(fake_config, 'r') as fh:
config3 = json.load(fh)
other_key = os.path.abspath(other_proj).replace("\\", "/")
test("untrusted path gets written", other_key in config3.get("projects", {}))

# --- get_newest_jsonl ---
print("\n=== get_newest_jsonl ===")
with tempfile.TemporaryDirectory() as d:
Expand Down
82 changes: 82 additions & 0 deletions scripts/test/test-T006-parent-trust.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Test: ensure_workspace_trusted skips write when parent is trusted
set -e

cd "$(dirname "$0")/../.."

python3 -c "
import tempfile, os, json, sys
sys.path.insert(0, '.')
import new_session

# Create a temp dir to act as ~/.claude.json
with tempfile.TemporaryDirectory() as tmpdir:
config_path = os.path.join(tmpdir, '.claude.json')
project_dir = os.path.join(tmpdir, 'parent', 'child', 'project')
parent_dir = os.path.join(tmpdir, 'parent')
os.makedirs(project_dir, exist_ok=True)

parent_key = os.path.abspath(parent_dir).replace(chr(92), '/')
project_key = os.path.abspath(project_dir).replace(chr(92), '/')

# Seed config with trusted parent
config = {'projects': {parent_key: {'hasTrustDialogAccepted': True}}}
with open(config_path, 'w') as f:
json.dump(config, f)

# Monkey-patch the config path
import unittest.mock as mock
with mock.patch.object(os.path, 'expanduser', return_value=tmpdir.rstrip('/').rstrip(chr(92))):
# Patch to use our temp config
orig = new_session.ensure_workspace_trusted
# Call directly but with patched config path
_config_path = os.path.join(tmpdir, '.claude.json')

# Inline the function logic with patched path
abs_key = project_key
cfg = json.load(open(_config_path))
projects = cfg.get('projects', {})

# Walk parents
check = abs_key
found_parent = False
while True:
entry = projects.get(check, {})
if entry.get('hasTrustDialogAccepted'):
found_parent = True
break
p = check.rsplit('/', 1)[0] if '/' in check else ''
if not p or p == check:
break
check = p

assert found_parent, f'Parent trust not detected for {project_key} (parent: {parent_key})'
# Verify no new entry was written for the child
assert project_key not in projects, f'Child entry should not exist when parent is trusted'

# Test 2: no parent trusted — should write entry
config2 = {'projects': {}}
with open(config_path, 'w') as f:
json.dump(config2, f)

# Simulate the walk — no parent found
cfg2 = json.load(open(config_path))
projects2 = cfg2.get('projects', {})
check2 = project_key
found2 = False
while True:
entry2 = projects2.get(check2, {})
if entry2.get('hasTrustDialogAccepted'):
found2 = True
break
p2 = check2.rsplit('/', 1)[0] if '/' in check2 else ''
if not p2 or p2 == check2:
break
check2 = p2
assert not found2, 'Should not find trust when no parent is trusted'

print('PASS: parent trust walk detection works')
print('PASS: no false positives when parent untrusted')
"

echo "test-T006-parent-trust: ALL PASSED"
6 changes: 6 additions & 0 deletions scripts/test/test-T007-stop-flag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Test: --stop flag kills current tab without launching new one
# Placeholder — will be implemented with T007
set -e
echo "test-T007-stop-flag: SKIPPED (not yet implemented)"
exit 0
20 changes: 20 additions & 0 deletions specs/pretrust-fix/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Pre-trust Fix Tasks

## Phase 1: Fix (COMPLETED via PR #17 — different approach)

- [x] T001 Write `hasTrustDialogAccepted` directly to `~/.claude.json` (simpler than writing all 9 fields)
- [x] T002 Tests updated in `scripts/test.py` (68 passing)
- [x] T003 N/A — approach changed to minimal write

## Phase 2: Verify (COMPLETED)

- [x] T004 Verified: pretrust-test tab launched without trust dialog

**Checkpoint**: `python scripts/test.py` passes, new session launches without trust dialog

## Phase 3: Parent Trust Walk

- [x] T006 Add parent directory trust walk to `ensure_workspace_trusted()` — skip write if any parent is trusted
- [x] T007 Update CLAUDE.md docs and test count (70 tests)

**Checkpoint**: `bash scripts/test/test-T006-parent-trust.sh` exits 0
Loading