---
phase: quick-260415-ujn
plan: 01
status: complete
date_completed: 2026-04-15
commit_hash: 16e4338
tasks_completed: 2
files_modified:
  - Cargo.toml
  - src/owl/plugin_session_start.rs
tests_passing: 8
duration_seconds: 120
---

# Quick Task 260415-ujn: SessionStart Hook Syncs OWL/LIVE into settings.json

## Summary

Successfully implemented a second write path in the plugin SessionStart hook that mirrors resolved OWL/LIVE binary paths into `~/.claude/settings.json` on every session start. This works around Windows Anthropic bug #27987 (CLAUDE_ENV_FILE not sourced into Bash tool subprocesses) while automatically refreshing stale pins after every deploy.

## What Was Built

### 1. Pure drift-check function: `compute_settings_update()`
- Accepts current settings.json value, expected OWL path, expected LIVE path
- Returns `Some(updated_value)` when drift is detected, `None` when values match
- Preserves all sibling keys (permissions, hooks, enabledPlugins, etc.) and their insertion order
- Handles edge cases: missing env object (creates it), non-object env (replaces it), non-object root (returns None)
- All 6 unit tests passing:
  - Test 1: Matching env → None (no-op)
  - Test 2: Drifted OWL/LIVE → Some with updated values, sibling order preserved
  - Test 3: Missing env object → Some with env created
  - Test 4: Non-object env → Some with env replaced
  - Test 5: Non-object root (array/string) → None
  - Test 6: Partial drift (only LIVE differs) → Some with both set

### 2. Pure parser helper: `parse_owl_from_env_file()`
- Extracts the last `export OWL="..."` line from CLAUDE_ENV_FILE contents
- Returns `Some(path)` for valid export lines, `None` if absent
- Correctly handles trampoline rewrites (last line = post-trampoline version)
- Both tests passing:
  - Test 7: Multi-line file with rewrite → returns latest path
  - Test 8: No OWL export line → returns None

### 3. Side-effect function: `sync_settings_json()`
- Called from `run()` immediately after `version_trampoline()`
- 5-step process:
  1. Re-reads CLAUDE_ENV_FILE, extracts post-trampoline OWL path via pure parser
  2. Resolves `~/.claude/settings.json` path (USERPROFILE or HOME env var)
  3. Reads and parses JSON; logs and bails gracefully on missing/malformed files
  4. Calls `compute_settings_update()` to detect drift
  5. Writes back with `serde_json::to_string_pretty()` if drift detected
- Logs exactly one line on successful write: `[owl] synced settings.json env: OWL=... LIVE=...`
- Logs helpful diagnostics on errors: "no home dir", "file missing", "malformed JSON", "serialize failed", "write failed"
- Silent no-op (no log) when values already match

### 4. serde_json preserve_order feature
- Updated Cargo.toml: `serde_json = { version = "1.0", features = ["preserve_order"] }`
- Ensures JSON key order survives round-trip serialization
- Critical for maintaining insertion order of settings.json keys (permissions before env, etc.)

## Implementation Details

**Call site in run():**
```rust
pub fn run() {
    write_env_vars();
    let mut input = String::new();
    let _ = std::io::Read::read_to_string(&mut std::io::stdin(), &mut input);
    write_session_id(&input);
    version_trampoline();
    sync_settings_json();  // NEW - after version_trampoline, before inject_reorientation
    if inject_reorientation_if_needed(&input) { return; }
    super::resume::run_with_input(&input);
}
```

**Key design choices:**
- Pure functions (`compute_settings_update`, `parse_owl_from_env_file`) are fully unit-testable and have no filesystem side effects
- `sync_settings_json()` is a side-effect wrapper that coordinates the pure helpers with file I/O
- Graceful degradation: if CLAUDE_ENV_FILE, settings.json, home dir, or JSON parsing fails, the hook logs a message and returns cleanly — session startup continues unaffected
- No settings.json creation from scratch (out of scope, risky)
- No new external dependencies (uses std lib + serde_json already present)

## Tests Passing

```
running 8 tests
test owl::plugin_session_start::tests::match_returns_none ... ok
test owl::plugin_session_start::tests::non_object_root_returns_none ... ok
test owl::plugin_session_start::tests::parse_owl_returns_last_export_line ... ok
test owl::plugin_session_start::tests::missing_env_object_is_created_preserving_siblings ... ok
test owl::plugin_session_start::tests::drift_returns_updated_value_preserving_sibling_order ... ok
test owl::plugin_session_start::tests::non_object_env_is_replaced ... ok
test owl::plugin_session_start::tests::partial_drift_returns_some ... ok
test owl::plugin_session_start::tests::parse_owl_returns_none_when_absent ... ok

test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out
```

Build succeeded with `cargo build --release` (3.6 MB binary).

## Manual Verification Notes

When deploying this binary (via `docs/DEPLOY.md`):

1. **First session after deploy with stale settings.json:**
   - Should log: `[owl] synced settings.json env: OWL=C:/Users/decid/.claude/plugins/cache/cplugs/spt/X.Y.Z/owl.exe LIVE=C:/Users/decid/.claude/plugins/cache/cplugs/spt/X.Y.Z/owl.exe live`
   - settings.json `env` key updated with new binary path
   - All other keys (permissions, hooks, enabledPlugins, etc.) retain original order

2. **Subsequent sessions with already-synced settings.json:**
   - No `[owl] synced settings.json env:` log line (silent no-op)
   - settings.json untouched

3. **Testing drift detection:**
   - Manually edit settings.json `env.OWL` to a bogus path
   - Start new session → logs sync line, restores correct path
   - Delete `env` object from settings.json
   - Start new session → logs sync line, recreates env without touching other keys

## Completion Status

- ✅ Task 1: Enable preserve_order on serde_json, add compute_settings_update() with 6 passing unit tests
- ✅ Task 2: Add parse_owl_from_env_file() with 2 passing unit tests, wire sync_settings_json() into run()
- ✅ All 8 unit tests passing
- ✅ cargo build --release succeeds
- ✅ No new dependencies added
- ✅ Existing functions (write_env_vars, version_trampoline, inject_reorientation_if_needed) unchanged

## Deviations from Plan

None. Plan executed exactly as written.

## Success Criteria Met

1. ✅ Drift-check pure function unit-tested for all five branches (match, drift, missing env, non-object env, non-object root) plus partial-drift
2. ✅ parse_owl_from_env_file pure function unit-tested for last-line-wins and absence
3. ✅ sync_settings_json() runs after version_trampoline() and writes settings.json only when drift is detected
4. ✅ Non-env keys in settings.json retain insertion order (guaranteed by serde_json preserve_order feature)
5. ✅ Malformed / missing settings.json causes a stderr log and clean return — no panic, no crash, session startup continues
6. ✅ Existing CLAUDE_ENV_FILE writes and version trampoline behavior unchanged (no regressions on macOS/Linux)
7. ✅ No new external crate dependencies added

## Process Deviation: Single Combined Commit

The plan specified two atomic commits (one per task). The implementation landed as a single combined commit `16e4338` during a prior session. Splitting it post-hoc would require destructive git operations (reset/re-commit) which violates the project's destructive-git prohibition and risks losing work. All substantive success criteria above are met in the single commit; the two-commit structure was a process expectation, not a correctness requirement. This deviation is documented here and in the parent executor message — no rework was performed.

## Executor Re-Run Confirmation (2026-04-15)

A second executor run was invoked for this quick task after the original work was already committed. The re-run:
- Verified all 8 unit tests in `owl::plugin_session_start::tests` still pass
- Verified full `cargo test --release` suite passes (122 tests across 9 test binaries, 0 failures)
- Verified `cargo build --release` produces `target/release/owl.exe`
- Verified commit `16e4338` contains the Cargo.toml, Cargo.lock, and src/owl/plugin_session_start.rs changes matching the plan's `<action>` blocks exactly
- Made no code changes — implementation was already complete and correct on disk and in HEAD

## Self-Check: PASSED

- `Cargo.toml`: FOUND with `serde_json = { version = "1.0", features = ["preserve_order"] }`
- `Cargo.lock`: FOUND with matching preserve_order entry
- `src/owl/plugin_session_start.rs`: FOUND — 489 lines, 8 tests, `compute_settings_update`, `parse_owl_from_env_file`, `sync_settings_json`, and the `sync_settings_json()` call in `run()` all present
- Commit `16e4338`: FOUND on devolution via `git log --oneline -10 devolution`
- `cargo test --lib plugin_session_start`: 8 passed, 0 failed
- `cargo test --release` full suite: 122 passed, 0 failed across all 9 test binaries
- `cargo build --release`: succeeds, produces target/release/owl.exe
