## Stub results — argv and termination, exercised before requesting the launch

Run on this node, harmless stubs only, **no Claude launch**. Script:
`scratchpad/stubtest.ps1`; output preserved in `scratchpad/stubout/`.

| Test | Result |
|---|---|
| **`Start-Process -ArgumentList`** (the construction in `04181e5`) | **BROKEN, measured.** The 4-element vector arrived as **16 arguments** — `-p`, `Run`, `one`, `shell`, `command`, `that`, … The prompt was shredded word by word. |
| **`ProcessStartInfo.ArgumentList`** (the fix) | **EXACT VECTOR MATCH, 4/4**, element-by-element, case-sensitive. |
| `Stop-Guarded` on a live process | `CONFIRMED-EXIT` after post-stop re-query |
| `Stop-Guarded` on an already-exited process | `ABSENT` |
| `Stop-Guarded` with a mismatched creation time | `IDENTITY-MISMATCH`, **and the process was verified still running** — it was not killed |
| `QUERY-FAILURE` / `EXIT-UNCERTAIN` | **Not fault-injected.** Making `Get-Process` fail for a reason other than absence needs a permission or WMI fault I will not manufacture on a shared box. The branches are present and distinct; exercised by inspection, not by injection. Stated rather than implied. |

So the argv blocker was real and is now fixed by construction rather than by assertion, and the
earlier "argv proof" would have been proving the broken construction against itself.

## Commands, as they will be run

```powershell
# --- setup -------------------------------------------------------------------
$ErrorActionPreference = 'Stop'
$RIG     = 'C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\19d48c98-d787-4436-90c2-da3f52339ae1\scratchpad\precheck'
$OUT     = "$RIG\out"
$WORK    = 'C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\61a5cd70-a460-42b8-9332-60f6bfe05a40\scratchpad\trust-probe31'
$TRACE   = 'C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-claude-spt\hook-trace.log'
$CONFIG  = $env:CLAUDE_CONFIG_DIR
if (-not $CONFIG) { throw 'CLAUDE_CONFIG_DIR unset - resolve the config root explicitly before running' }
$SLUG       = ($WORK -replace '[:\\]', '-')
$PROJDIR    = Join-Path (Join-Path $CONFIG 'projects') $SLUG
$DEADLINE_S = 180      # ONE deadline, measured from launch
$CLEANUP_S  =  30      # separate bounded interval, process cleanup only
$TRIGGER_S  =  60      # trigger wait, spent INSIDE the deadline, never added to it
$NONCE      = "PRECHECK-" + (Get-Date -Format 'yyyyMMdd-HHmmss')
New-Item -ItemType Directory -Force -Path $OUT | Out-Null

# WORK must be created BY THIS ATTEMPT. A pre-existing WORK is refused rather than reused, so
# nothing here can adopt - or later disturb - a directory somebody else owns.
if (Test-Path $WORK) {
    "WORK already exists: $WORK - refusing to reuse or clean it" | Out-File "$OUT\precheck.ABORT.txt"
    throw 'pre-existing WORK - aborting'
}

# RE-VERIFY the trusted chain read-only. Report and STOP on any finding; never clean it up.
$bad = @(); $probe = $WORK
while ($probe) {
    foreach ($f in 'CLAUDE.md','AGENTS.md','.claude') {
        if (Test-Path (Join-Path $probe $f)) { $bad += (Join-Path $probe $f) }
    }
    $parent = Split-Path $probe -Parent
    if (-not $parent) { break }      # Split-Path of the drive root is EMPTY: the root is checked by
    $probe = $parent                 #   this iteration before the loop ends, not skipped
}
$bad = $bad | Where-Object { $_ -notlike "$env:USERPROFILE\.claude*" }   # user-level layer is expected
if ($bad) { $bad | Out-File "$OUT\precheck.ABORT.txt"; throw 'project-scoped config on the chain - reporting, not cleaning' }

# --- argv: BUILT correctly, then PROVEN exactly against a harmless stub -------
# ProcessStartInfo.ArgumentList escapes per element. Start-Process -ArgumentList re-joins and was
# MEASURED to shred this prompt into 16 arguments (see Stub results), so it is not used anywhere.
$PROMPT = "Run one shell command that prints exactly $NONCE, then reply with exactly $NONCE"
$ARGS_V = @('-p', $PROMPT, '--output-format', 'json')

function Start-Exact([string]$exe, [string[]]$argv, [string]$stdout, [string]$stderr) {
    $psi = [Diagnostics.ProcessStartInfo]::new()
    $psi.FileName = $exe
    foreach ($a in $argv) { $psi.ArgumentList.Add($a) }   # per-element escaping; no join
    $psi.WorkingDirectory = $WORK
    $psi.UseShellExecute = $false
    $psi.RedirectStandardOutput = $true; $psi.RedirectStandardError = $true
    $pr = [Diagnostics.Process]::Start($psi)
    return $pr
}

$stub = "$OUT\argv-stub.ps1"
Set-Content -Path $stub -Encoding UTF8 -Value '$i=0; foreach ($a in $args) { [Console]::Out.WriteLine("<<$i>>$a<<END>>"); $i++ }'
$sp = Start-Exact 'powershell' (@('-NoProfile','-File',$stub) + $ARGS_V) $null $null
$stubOut = $sp.StandardOutput.ReadToEnd(); $sp.WaitForExit()
$stubOut | Out-File "$OUT\precheck.argv.txt"
$recv = @()
foreach ($line in ($stubOut -split "`r?`n")) { if ($line -match '^<<(\d+)>>(.*)<<END>>$') { $recv += $Matches[2] } }
# EXACT vector equality - count and every element, case-sensitive. Not a substring match.
$ok = ($recv.Count -eq $ARGS_V.Count)
if ($ok) { for ($i=0; $i -lt $ARGS_V.Count; $i++) { if ($recv[$i] -cne $ARGS_V[$i]) { $ok = $false } } }
if (-not $ok) {
    "ARGV PROOF FAILED expected=$($ARGS_V.Count) received=$($recv.Count)`n$($recv -join "`n")" |
        Out-File "$OUT\precheck.ABORT.txt"
    throw 'exact argv not proven - not launching'
}

# --- pre-launch inventory: whatever already exists is NOT ours ----------------
New-Item -ItemType Directory -Force -Path $WORK | Out-Null   # created BY THIS ATTEMPT (refused above if present)
$PRE = @{}
if (Test-Path $PROJDIR) {
    Get-ChildItem "$PROJDIR\*.jsonl" -ErrorAction SilentlyContinue | ForEach-Object { $PRE[$_.FullName] = $true }
}
$PRE.Keys | Sort-Object | Out-File "$OUT\precheck.preexisting.txt"
Copy-Item $TRACE "$OUT\hook-trace.pre.log"; Copy-Item "$TRACE.1" "$OUT\hook-trace.1.pre.log"
$tf = Get-Item $TRACE; $MARK = $tf.Length; $MARKID = $tf.CreationTimeUtc

# --- terminator: absence, identity mismatch, query failure and confirmed exit
# all kept distinct - BEFORE the stop and AFTER it. A post-stop query failure yields
# EXIT-UNCERTAIN, never CONFIRMED-EXIT.
function Stop-Guarded($id, $start, $label) {
    $live = $null; $queryFailed = $false
    try { $live = Get-Process -Id $id -ErrorAction Stop }
    catch [Microsoft.PowerShell.Commands.ProcessCommandException] { $live = $null }   # genuinely absent
    catch { $queryFailed = $true }                                                    # the query itself failed
    if ($queryFailed)               { return "QUERY-FAILURE $label pid=$id residual_start=$($start.ToString('o')) - NOT killed, identity preserved" }
    if (-not $live)                 { return "ABSENT $label pid=$id - already exited, nothing killed" }
    if ($live.StartTime -ne $start) { return "IDENTITY-MISMATCH $label pid=$id expected=$($start.ToString('o')) actual=$($live.StartTime.ToString('o')) - PID RECYCLED, NOT killed" }
    Stop-Process -Id $id -Force -ErrorAction SilentlyContinue
    Start-Sleep -Milliseconds 300
    $after = $null; $afterFailed = $false
    try { $after = Get-Process -Id $id -ErrorAction Stop }
    catch [Microsoft.PowerShell.Commands.ProcessCommandException] { $after = $null }
    catch { $afterFailed = $true }
    if ($afterFailed)                            { return "EXIT-UNCERTAIN $label pid=$id - post-stop query FAILED; exit NOT confirmed" }
    if ($after -and $after.StartTime -eq $start) { return "KILL-UNCONFIRMED $label pid=$id - still present after Stop-Process" }
    return "CONFIRMED-EXIT $label pid=$id start=$($start.ToString('o'))"
}

# Attribution, used identically by the TRIGGER and by the HARVEST.
function Get-AttributedTranscripts {
    if (-not (Test-Path $PROJDIR)) { return @() }
    return @(Get-ChildItem "$PROJDIR\*.jsonl" -ErrorAction SilentlyContinue |
        Where-Object { -not $PRE.ContainsKey($_.FullName) } |
        Where-Object { Select-String -Path $_.FullName -Pattern ([regex]::Escape($NONCE)) -Quiet })
}

# --- the one launch, with cleanup in finally ---------------------------------
$pid0 = $null; $st0 = $null; $firstError = $null; $MINE = @()
try {
    $p = Start-Exact 'claude' $ARGS_V $null $null
    $pid0 = $p.Id; $st0 = $p.StartTime
    $LAUNCH_AT = Get-Date
    $DEADLINE  = $LAUNCH_AT.AddSeconds($DEADLINE_S)     # the ONE clock, from launch
    "LAUNCH pid=$pid0 start=$($st0.ToString('o')) deadline=$($DEADLINE.ToString('o'))" |
        Out-File "$OUT\precheck.launches.txt"

    # Trigger: ATTRIBUTED transcripts only - same nonce rule the harvest uses, so a prior or
    # unrelated session can never serve as trigger evidence.
    # NEITHER PATH ESTABLISHES A LIVE TURN: this is a RECORDING time, and buffering may place the
    # observation after the live turn ended. The bound firing says nothing about turn state at all.
    $why = 'trigger-bound-reached'
    $triggerEnd = $LAUNCH_AT.AddSeconds($TRIGGER_S)
    while ((Get-Date) -lt $triggerEnd -and (Get-Date) -lt $DEADLINE) {
        if (Get-AttributedTranscripts | Where-Object { Select-String -Path $_.FullName -Pattern '"type"\s*:\s*"tool_use"' -Quiet }) {
            $why = 'attributed-tool-evidence-recorded'; break
        }
        Start-Sleep -Milliseconds 400
    }
    "KILL-TRIGGER path=$why at=$((Get-Date).ToString('o')) window=UNESTABLISHED" |
        Out-File "$OUT\precheck.killtrigger.txt"
    Stop-Guarded $pid0 $st0 'trigger' | Out-File "$OUT\precheck.kill.txt"

    $remain = [int][Math]::Max(0, ($DEADLINE - (Get-Date)).TotalSeconds)   # SAME deadline, never a fresh one
    if ($remain -gt 0) { Wait-Process -Id $pid0 -Timeout $remain -ErrorAction SilentlyContinue }

    # --- harvest: rotation-aware, attributed ---------------------------------
    $tf2 = Get-Item $TRACE
    if ($tf2.Length -lt $MARK -or $tf2.CreationTimeUtc -ne $MARKID) {
        "ROLLED mark_len=$MARK now_len=$($tf2.Length) - EVIDENCE UNAVAILABLE (not a silent-hook finding)" |
            Out-File "$OUT\precheck.trace.txt"
    } else {
        $fs = [IO.File]::Open($TRACE,'Open','Read','ReadWrite'); $fs.Seek($MARK,'Begin') | Out-Null
        (New-Object IO.StreamReader($fs)).ReadToEnd() | Out-File "$OUT\precheck.trace.txt"; $fs.Close()
    }
    $MINE = Get-AttributedTranscripts
    $MINE | Select-Object FullName, Length, LastWriteTimeUtc | Out-File "$OUT\precheck.attributed.txt"
    $MINE | ForEach-Object { Copy-Item $_.FullName (Join-Path $OUT "precheck.$($_.Name)") }   # evidence copied, originals RETAINED
}
catch {
    $firstError = $_                     # preserved; never masked by a cleanup failure
    "FIRST-ERROR $($_.Exception.Message)" | Out-File "$OUT\precheck.error.txt"
}
finally {
    # Process cleanup ALWAYS runs, even if the harvest or a file read threw above.
    $cleanup_end = (Get-Date).AddSeconds($CLEANUP_S)
    try {
        if ($pid0) {
            $r = Stop-Guarded $pid0 $st0 'cleanup'
            $r | Out-File "$OUT\precheck.kill.txt" -Append
            if ($r -match '^(QUERY-FAILURE|EXIT-UNCERTAIN|KILL-UNCONFIRMED)') {
                "RESIDUAL pid=$pid0 start=$($st0.ToString('o')) - exit NOT confirmed; no artifact removed" |
                    Out-File "$OUT\precheck.residual.txt"
            }
        }
        if ((Get-Date) -ge $cleanup_end) { 'CLEANUP-BOUND REACHED' | Out-File "$OUT\precheck.cleanup.txt" -Append }
    }
    catch { "CLEANUP-FAILURE $($_.Exception.Message)" | Out-File "$OUT\precheck.cleanup.txt" -Append }
    if ($firstError) { throw $firstError }   # the original failure is what surfaces
}
```

## Cleanup — process only. Nothing is deleted.

**No file is removed by this check.** `WORK` is retained, and so are the transcripts, after their
evidence copies land in `$OUT`. That removes the whole class of risk doyle named: there is no
recursive delete to get wrong, nothing pre-existing can be destroyed, and **no artifact is ever
removed while process exit is uncertain** — because no artifact is removed at all. A single check
does not need the tidying.

| Item | Action |
|---|---|
| The spawned process | Terminated under `Stop-Guarded`, inside `finally`, within a 30 s bound |
| `QUERY-FAILURE` / `EXIT-UNCERTAIN` / `KILL-UNCONFIRMED` | Recorded in `precheck.residual.txt` with the PID and creation time, for follow-up |
| `WORK` | **Retained** (created by this attempt; a pre-existing one aborts the run) |
| Transcripts under the slug | **Retained**; copies preserved in `$OUT` |
| Pre-existing records | **Never touched.** Listed in `precheck.preexisting.txt` |
| Trust / settings / whole-config | **Never written, never restored** |

Every item is reported as performed rather than asserted as a whole. Retained artifacts are listed so
a later cleanup, if anyone wants one, is a separate decision with its own evidence.

