Skip to content

v1.6.22

Choose a tag to compare

@marmutapp marmutapp released this 21 May 03:36
· 14 commits to main since this release

feat(claudecode,hook): per-turn effort.level capture for Claude Code on both Linux CLI and Windows Desktop

Closes a gap operator-reported 2026-05-21: Claude Code's effort
dropdown (Max / Extra High / High / Medium / Low) was changing real
behavior turn-to-turn, but the dashboard's per-action Effort column
(landed in v1.6.18) stayed empty for every claude-code row. Three
distinct fragilities lined up underneath that one symptom; each is
fixed in this ship.

Why the JSONL alone can't carry effort. Exhaustive JSON-key scan
across hundreds of .jsonl files on both Windows and Linux corpora
returns zero hits for effort / reasoning_effort / budget_tokens /
thinking_budget. Effort is a request-side
thinking: {budget_tokens: N} parameter that the Anthropic API never
echoes back into the response stream. Per
code.claude.com/docs/en/hooks, effort.level IS exposed to the
PreToolUse / PostToolUse / Stop / SubagentStop hook payloads — and
that's the only per-turn source. Observer registers all four events
already, but HandleApprove was discarding the payload.

What this ship adds:

  • Migration 026 — new claudecode_effort sidecar table keyed
    (session_id, tool_use_id) storing effort_level, event_name,
    received_at. The Anthropic toolu_xxx block ID is already the
    source_event_id for tool_use rows in actions, so joins are
    natural without a schema change to actions.
  • store.UpsertClaudecodeEffort — single in-tx upsert that
    populates the sidecar AND runs an UPDATE on any matching
    already-inserted action row's metadata.effort_level. Race-safe in
    either ordering: hook fires before JSONL ingest (sidecar lookup
    catches it on parse) or after (UPDATE stamps the row immediately).
  • recordClaudecodeEffort in cmd/observer/hook.go — extracts
    (session_id, tool_use_id, effort.level) from PreToolUse + new
    PostToolUse dispatch; no-ops cleanly when effort.level is absent
    (which the docs say happens on models that don't support effort).
  • claudecode.Adapter.WithEffortLookup — JSONL parse-time
    enrichment via per-session cached map; fail-safe on lookup errors
    (parse still returns events).
  • claude-code-windows registration target — mirrors
    cursor-windows. Writes hooks into a Windows-side
    .claude/settings.json with wsl.exe -d <distro> -- <linux-bin> so
    Claude Desktop on Windows can fire hooks into the WSL-side observer
    binary. Auto-surfaced via Registry.Installed() when crossmount
    detects a Windows-side .claude/.

fix(hook): three E2E fragilities that broke the chain on first ship

Discovered during empirical Desktop-side validation:

  1. Git Bash MSYS path translation — Claude Code on Windows runs
    hook commands through Git Bash (per the upstream docs). Git Bash's
    MSYS layer auto-rewrites POSIX-shaped /home/... arguments into
    C:/Program Files/Git/home/... before they reach the program being
    spawned, so wsl.exe -d Ubuntu-20.04 -- /home/.../bin/observer
    became wsl.exe -d Ubuntu-20.04 -- C:/Program Files/Git/home/.../ bin/observer, which wsl.exe could not find inside the Linux distro,
    exit-127 every fire. JSONL attachment records captured the symptom
    verbatim:

    {"type":"hook_non_blocking_error", "exitCode":127,
     "stderr":"/bin/bash: C:/Program Files/Git/home/.../observer: No
     such file or directory"}
    

    Fix: prefix every registered Windows-side wsl.exe command with
    MSYS_NO_PATHCONV=1 so Git Bash skips POSIX→Win32 conversion. The
    env-var assignment is bash-only — silently ignored by macOS/Linux
    sh -c and by cmd.exe — so it's safe to set unconditionally on the
    Windows registrars without OS branching. Same fix mirrored to
    cursor-windows (Cursor on Windows uses the same Git Bash hook
    execution path). isObserverWindowsClaudeEntry /
    isObserverWindowsCursorEntry updated to recognise both the old
    prefix-free and new MSYS-prefixed shapes, so refresh-on-drift
    silently upgrades existing user registrations.

  2. selectTools whitelist missing -windows variants
    cmd/observer/init.go::selectTools had a local supported map
    hard-coded to {claude-code, cursor, codex} that silently dropped
    claude-code-windows and cursor-windows from observer init --all even though both registrars existed and Installed()
    surfaced them. Auto-register-on-start was unaffected (uses
    hookSupported directly), but the explicit init path needed the
    fix. Cursor-windows had been quietly broken for users running
    observer init explicitly; bonus fix included.

  3. db.Open deadlined by HookTimeout
    recordClaudecodeEffort initially fenced both db.Open and the
    upsert under cfg.Observer.Hooks.HookTimeout() (~250-500ms write-
    side budget). While the long-running daemon holds the WAL hot, a
    per-process hook invocation's db.Open reliably busts that budget
    on the quick_check integrity probe and silently drops the effort
    write. Split the deadlines (mirrors the cursor/codex hook
    handlers): unbounded context.Background() for db.Open,
    HookTimeout only on the actual write. Sidecar writes now land in
    <50ms in steady state.

Migration

  • db.Open applies migration 026 (new claudecode_effort table)
    automatically on next daemon start. Zero risk: pure additive
    CREATE — no existing data touched.
  • Existing claude-code + cursor hook registrations on the Linux
    home are untouched. Windows-side settings.json / hooks.json
    entries previously written by Phase 2 / the cursor-windows v1.4.45
    ship get refreshed in place with the new MSYS_NO_PATHCONV=1
    prefix on the next observer start or observer init --all --force (refresh-on-drift recognises both shapes as ours).

What this doesn't fix

  • The target session bd834194-1b9c-480f-bd14-f126a5f35a55 that
    prompted this investigation cannot be retroactively backfilled
    no hooks fired during its lifetime because the Windows-side
    settings.json hadn't been written yet. Only sessions started AFTER
    the registration write AND after Claude Desktop is restarted to
    re-read settings.json will capture per-turn effort.
  • Models that don't support the effort parameter (per Anthropic's
    docs) will still leave the column blank. The hooks fire and exit 0;
    recordClaudecodeEffort no-ops cleanly when effort.level is empty
    in the payload. Opus 4.7 and other extended-thinking-capable models
    do emit it.

Verified

  • Empirical end-to-end on Claude Desktop v2.1.138 (2026-05-20, fresh
    install): hooks fire exit 0; claudecode_effort sidecar populates;
    dashboard Effort column shows the per-turn value across Max → High
    → Low ladder changes.
  • go test -race ./... — 51 packages OK (one inherent concurrent-
    migration test flake under 51-package parallel load; passes 5/5 in
    isolation).
  • go vet ./... — clean.

Docs

  • New docs/claudecode-hook-capture.md — verified Claude Code hook
    envelope, dual-registration model, MSYS gotcha, sidecar contract.
  • docs/provider-mapping.mdclaude-code-windows cell added,
    MSYS_NO_PATHCONV note appended to both Windows registrars' rows.

Downloads

Pre-built binaries for each supported platform are attached below. Linux variants bundle antigravity-bridge.exe next to the observer binary for WSL2 users of the Antigravity adapter.

Platform Asset
Linux x86_64 observer-v1.6.22-linux-x64.tar.gz
Linux arm64 observer-v1.6.22-linux-arm64.tar.gz
macOS x86_64 (Intel) observer-v1.6.22-darwin-x64.tar.gz
macOS arm64 (Apple Silicon) observer-v1.6.22-darwin-arm64.tar.gz
Windows x86_64 observer-v1.6.22-win32-x64.zip

Verify with sha256sum -c SHA256SUMS (or shasum -a 256 -c SHA256SUMS on macOS) from the directory containing the downloads.

Also available via npm: npm install -g @superbased/observer@1.6.22