sinter-io 0.52.2

sinter command-line interface
# sinter-first: Claude Code enforcement hooks (managed by `sinter install`;
# edits are overwritten). Injects sinter routing context when the cwd (or
# an ancestor, git-style) has a built sinter graph.
# SECURITY INVARIANT (absolute): this script must NEVER emit
# permissionDecision "allow" — that would auto-approve the ENTIRE Bash
# command, including anything destructive sharing a line with a grep.
# "deny" only blocks and is safe; default modes stay context-injection
# only and emit no permissionDecision at all.
# Modes:
#   prompt   — UserPromptSubmit: one router line, once per session
#   grep     — PreToolUse(Bash): nudge on recursive-search commands
#              (rg/ag/grep -r/git grep/find -name), plus a git-archaeology
#              nudge on `git log -S/-G`; each class at most once per session
#   greptool — PreToolUse(Grep): nudge for the dedicated Grep tool (shares
#              the search class with grep)
#   grep-strict / greptool-strict — opt-in strict variants: the FIRST
#              matching search of a session is denied with a redirect to
#              sinter; the retry gets one advisory nudge, and later searches
#              in that session are silent. Sinter-first, grep-second, never
#              grep-never. No session_id → nudge only.
param([string]$Mode)

$root = $null
$d = (Get-Location).Path
while ($d) {
    if (Test-Path (Join-Path $d '.sinter/graph.redb')) { $root = $d; break }
    $parent = Split-Path $d -Parent
    if (-not $parent -or $parent -eq $d) { break }
    $d = $parent
}
if (-not $root) { exit 0 }

$Nudge = 'sinter graph: unfamiliar repo -> map; vague discovery -> ask; exact symbol -> query/show; relations -> affected/deps/path; text inside a blast radius -> grep --within affected(SYM); negative proof -> unresolved, with incomplete coverage reported as not_proven. Grep remains for repo-wide content; for function bodies use show --body.'
$GitNudge = 'sinter graph: use impact <rev-range> for changed symbols, downstream effects, and tests; use overlap for collision risk; add --workspace for cross-repo analysis.'

$DenyReason = 'This repo has a sinter graph. Run sinter map first if unfamiliar; use sinter ask for vague discovery, sinter query/show for exact symbols, sinter affected/deps/path for relations, sinter grep --within affected(SYM) to search text inside a blast radius, sinter unresolved for negative proofs (incomplete coverage is not_proven), or sinter impact for diffs. If insufficient, rerun this exact search.'

function Emit([string]$Text) {
    Write-Output ('{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"' + $Text + '"}}')
}

# Return a marker path for a valid session without placing the raw session ID
# in the filesystem. The platform temp directory is per-user on Windows.
function Get-SessionMarkerPath([string]$Class, [string]$InputJson) {
    $sid = ''
    try { $sid = ([string]($InputJson | ConvertFrom-Json).session_id) } catch { $sid = '' }
    if ([string]::IsNullOrWhiteSpace($sid)) { return $null }
    $sha = [Security.Cryptography.SHA256]::Create()
    try { $hash = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($sid)) } finally { $sha.Dispose() }
    $token = -join ($hash | ForEach-Object { $_.ToString('x2') })
    $markerDir = Join-Path ([IO.Path]::GetTempPath()) 'sinter-hooks'
    try { New-Item -ItemType Directory -Path $markerDir -Force | Out-Null } catch { return $null }
    return Join-Path $markerDir "$Class-$token"
}

# Return true only for the first marker in a valid session, false for an
# existing marker, and null when no safe marker can be created. CreateNew is
# atomic across concurrent hook processes and never follows an existing file.
function New-SessionMarker([string]$Class, [string]$InputJson) {
    $marker = Get-SessionMarkerPath $Class $InputJson
    if (-not $marker) { return $null }
    try {
        $stream = [IO.File]::Open($marker, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
        $stream.Dispose()
        return $true
    }
    catch [IO.IOException] { return $false }
    catch { return $null }
}

function Test-StrictDeny([string]$InputJson) {
    return (New-SessionMarker 'strict' $InputJson) -eq $true
}

function Emit-Once([string]$Class, [string]$InputJson, [string]$Text) {
    $first = New-SessionMarker $Class $InputJson
    if ($null -eq $first -or $first) { Emit $Text }
}

function EmitDeny {
    Write-Output ('{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"' + $DenyReason + '"}}')
}

switch ($Mode) {
    'prompt' {
        $raw = ''
        try { $raw = [Console]::In.ReadToEnd() } catch { $raw = '' }
        if ((New-SessionMarker 'prompt' $raw) -eq $false) { exit 0 }
        Write-Output 'This repo has a sinter graph. Unfamiliar repo: sinter map first. Then use ask for vague discovery; query/show for exact symbols; affected/deps/path for relations; grep --within affected(SYM) for text inside a blast radius; impact --expect SYM for unfinished refactors; unresolved for negative proofs (incomplete coverage is not_proven); impact/overlap for changes; workspace/--workspace across repos. Use ensure/doctor/scip for setup or repair; show --body for function bodies.'
    }
    { $_ -in 'grep', 'grep-strict' } {
        $raw = ''
        try { $raw = [Console]::In.ReadToEnd() } catch { $raw = '' }
        $cmd = ''
        try { $cmd = ($raw | ConvertFrom-Json).tool_input.command } catch { $cmd = '' }
        if (-not $cmd) { exit 0 }
        if ($cmd -match '(^|[|;& ])(rg |ag |git +grep|(xargs|-exec) +(grep|rg)|grep +(-[a-zA-Z]*[rR]|.* -[rR])|find .* -i?name)') {
            if ($Mode -eq 'grep-strict' -and (Test-StrictDeny $raw)) { EmitDeny } else { Emit-Once 'search' $raw $Nudge }
        }
        # Git archaeology stays advisory in both modes.
        elseif ($cmd -match '(^|[|;& ])git +log .*-[SG]') { Emit-Once 'git' $raw $GitNudge }
    }
    { $_ -in 'greptool', 'greptool-strict' } {
        $raw = ''
        try { $raw = [Console]::In.ReadToEnd() } catch { $raw = '' }
        if ($Mode -eq 'greptool-strict' -and (Test-StrictDeny $raw)) { EmitDeny } else { Emit-Once 'search' $raw $Nudge }
    }
}
exit 0