# Session Isolation — Verification Report

Verified 2026-05-12. All tests run against v1.2.0 of `gemma-bedrock-monitor` loaded via `--plugin-dir` on a developer machine (WSL2, Claude Code, Anthropic API + AWS Bedrock).

## Background: The Flaw

Claude Code maintains a full JSONL transcript of every conversation. `claude --resume` replays that transcript to the active API endpoint. If a session was created on Bedrock (where data stays in AWS) and is resumed on the Anthropic API, the full history — including database query results and client data — is sent to Anthropic's servers.

This is not a theoretical risk: any developer who runs database work on Bedrock and then runs `claude --resume` (or uses `/resume` from the in-session picker) without re-enabling Bedrock would trigger the leak.

## The Solution (v1.2.0)

Three new hook scripts enforce isolation:

| Script | Hook | Can hard-block? | What it does |
|--------|------|-----------------|--------------|
| `session-isolation-start.sh` | `SessionStart` | No | On Bedrock: write taint marker. On Anthropic + tainted resume: write poison flag, inject `additionalContext`. |
| `session-isolation-prompt.sh` | `UserPromptSubmit` | Yes | Check poison flag or live taint marker. Block with `{"decision":"block"}` if found. |
| `session-isolation-tool.sh` | `PreToolUse` | Yes | Same check, blocks with `{"permissionDecision":"deny"}`. |

State is stored in `${CLAUDE_PLUGIN_DATA}/tainted-transcripts/` and `${CLAUDE_PLUGIN_DATA}/poisoned-sessions/`. Keys are the sha256 of the transcript path.

## Open Questions Before Release

Four questions needed verification before shipping:

| # | Question | Risk if wrong |
|---|----------|---------------|
| Q1 | Does `claude --resume` send history to Anthropic before the first user prompt? | If yes, the `UserPromptSubmit` block arrives too late — the initial replay already leaked. |
| Q2 | Does `/resume` inside a running Anthropic session fire a fresh `SessionStart`? | If no, Layer 1 (poison flag) won't be written; we rely entirely on Layer 2's live taint check. |
| Q3 | Does `/clear` truncate the JSONL or create a new session? | Determines whether taint markers should be kept after `/clear`. |
| Q4 | Is `CLAUDE_PLUGIN_DATA` set in hook subprocesses, and does it point to the right directory? | If unset, scripts fall back to a hardcoded path that may differ from the actual state dir. |

## Test Environment

- Claude Code with `--plugin-dir` pointing to local plugin checkout
- `mitmproxy` 12.2.3 used for HTTPS traffic inspection (Q1)
- Diagnostic logging added temporarily to `session-isolation-start.sh` (removed before release)
- Marketplace-installed copy of the plugin disabled for the duration to avoid duplicate hook firing

## Test Results

### Test 1 — Q4: `CLAUDE_PLUGIN_DATA` propagation

**Result: PASS**

`CLAUDE_PLUGIN_DATA` is always set in hook subprocesses. The suffix depends on how the plugin is loaded:
- Loaded via `--plugin-dir` → suffix is `inline` → state at `~/.claude/plugins/data/gemma-bedrock-monitor-inline/`
- Loaded from marketplace → suffix is `gemma-toolkit` → state at `~/.claude/plugins/data/gemma-bedrock-monitor-gemma-toolkit/`

The fallback path in the scripts (`$HOME/.claude/plugins/data/gemma-bedrock-monitor`, no suffix) is never reached in practice. State is always consolidated within a single directory per load context.

### Test 2 — Basic taint marking

**Result: PASS**

Every Bedrock `SessionStart` writes a correctly-named marker file:
```
~/.claude/plugins/data/gemma-bedrock-monitor-<suffix>/tainted-transcripts/<sha256(transcript_path)>
```

Verified by computing the expected sha256 and confirming the file exists.

### Test 3 — Q3: `/clear` semantics

**Result: PASS — Branch C (new session)**

`/clear` creates a **new session** with a new session ID and JSONL file. The old JSONL is left unchanged on disk. `SessionStart` fires with `source=clear` pointing to the new JSONL.

Implications:
- Old taint markers remain attached to the old transcript path (conservative, correct — the old file still exists)
- The new post-clear session gets its own taint marker immediately (verified)
- Resuming the old transcript would still be blocked; resuming the new one on Bedrock and then switching would be blocked via its own taint marker

### Test 4 — Core resume block

**Result: PASS**

`claude --resume <bedrock-session-id>` on the Anthropic API:
1. `SessionStart` fires with `source=resume`, `CLAUDE_CODE_USE_BEDROCK=''`
2. Taint marker found → poison flag written for the session
3. First user prompt blocked immediately:
   ```
   UserPromptSubmit operation blocked by hook:
   Data isolation: this transcript was previously active on AWS Bedrock and may
   contain client data. Sending it through the Anthropic API is blocked.

   To continue this conversation:
     1. Exit this session
     2. Run: bedrock-on
     3. Run: claude --resume
   ```
4. No model response. No history sent to Anthropic.

### Test 5 — Q1: No pre-flight POST

**Result: PASS — no transcript replay before first user prompt**

Traffic captured with `mitmproxy` while resuming a Bedrock session on the Anthropic API (plugin disabled, to allow the resume to proceed unblocked).

During a 30-second wait after `claude --resume` (before any user input), the only POST to `api.anthropic.com/v1/messages` was a quota ping:

```json
{
  "model": "claude-haiku-4-5-20251001",
  "max_tokens": 1,
  "messages": [{"role": "user", "content": "quota"}]
}
```

8 input tokens. No conversation history included.

After typing `hi`, a second POST to `/v1/messages` appeared — that one does include the full transcript.

**Conclusion**: the `UserPromptSubmit` block intercepts transcript transmission at exactly the right point. There is no pre-flight replay.

### Test 6 — Q2: `/resume` in-session fires `SessionStart`

**Result: PASS — Branch A (SessionStart fires)**

Steps taken:
1. Started a fresh Anthropic session with `--plugin-dir`
2. Sent a message (`Say hi`)
3. Used the in-session `/resume` picker and selected a Bedrock-tainted session

Result: `SessionStart` fired immediately with `source=resume`, `CLAUDE_CODE_USE_BEDROCK=''`, and the correct `transcript_path`. Poison flag was written. The next prompt was blocked.

This confirms that both paths to resuming a tainted session (command-line `--resume` and in-session `/resume` picker) are covered by Layer 1.

### Test 7 — Regression (no false positives)

**All four scenarios passed without spurious blocks:**

| Scenario | Result |
|----------|--------|
| Fresh Anthropic session, no resume | Normal response, no block |
| Fresh Bedrock session, no resume | Normal response, no block |
| Resume Bedrock session on Bedrock | Normal response, no block |
| Resume Anthropic-only session on Anthropic | Normal response, no block |

No poison flags written for any of these scenarios.

## Go/No-Go Decision

All tests passed. **Shipped as v1.2.0.**

## Residual Risk

None identified. The `UserPromptSubmit` block intercepts at the right point (Q1 confirmed no pre-flight replay). Both resume paths fire `SessionStart` (Q2 confirmed). The only scenario where this plugin does not protect you is if you explicitly bypass the block (which would require modifying the plugin).

---

## Windows Verification (Native PowerShell + Git for Windows)

Verified 2026-05-13. All tests run against v1.2.1 of `gemma-bedrock-monitor` installed at user scope via `claude plugin install` on a native Windows machine (Claude Code v2.1.81, Git for Windows 2.54.0 Cygwin bash, PowerShell 5.1).

### Key findings

**Root cause of Windows incompatibility (pre-v1.2.1):** Claude Code expands `${CLAUDE_PLUGIN_ROOT}` as a template variable before spawning the hook subprocess, producing a Windows path like `C:\Users\...\1.2.0`. When passed as a *file-path argument* to `bash.exe`, this path fails on Git Bash/Cygwin because command-line path arguments are not auto-translated. However, env var *values* are auto-translated by Cygwin: `$CLAUDE_PLUGIN_ROOT` in the subprocess env is already `/c/Users/...`. The `bash -c '...$CLAUDE_PLUGIN_ROOT...'` wrapper exploits this.

**Prerequisites confirmed for Windows:**
- Git for Windows ≥ 2.54.0 (provides Cygwin bash at `C:\Program Files\Git\bin\bash.exe`)
- `C:\Program Files\Git\bin` must be prepended to PATH *before* `C:\Windows\System32` (which contains the WSL bash launcher). WSL bash strips backslashes from path arguments; Git Bash handles them correctly.
- `jq` installed and visible to Git Bash (install via `winget install jqlang.jq`, verify with `bash -c "jq --version"`)

### Test results

| Test | Description | Result |
|------|-------------|--------|
| T1 | Bedrock startup writes taint marker | **PASS** |
| T2 | sha256 hash consistent across platforms (Windows-format path) | **PASS** |
| T3 | `/clear` semantics | not re-tested (platform-agnostic bash logic, covered by WSL T3) |
| T4 | Core resume block — resume Bedrock session on Anthropic API blocked | **PASS** |
| T5 | In-session `/resume` picker blocks tainted session | **PASS** |
| T6a | Fresh Anthropic session — not blocked | **PASS** |
| T6b | Fresh Bedrock session — not blocked | **PASS** |
| T6c | Resume Bedrock on Bedrock — not blocked | **PASS** |
| T6d | Resume Anthropic on Anthropic — not blocked | **PASS** |

### Verified platforms

| Platform | Date | Version | Result |
|----------|------|---------|--------|
| WSL2 (Ubuntu, Anthropic API + Bedrock) | 2026-05-12 | v1.2.0 | All pass |
| Native Windows (PowerShell, Git for Windows Cygwin bash) | 2026-05-13 | v1.2.1 | All pass |
