SLOPSHOPPER

harnu-probe

Test-only zero-model driver for the real-CLI suites: answers /harnu-probe with what it observes.

newcommand
★ 2v0.0.1MITupdated 2026-10-07junielton/harnu/tests/cli/fixtures/probe-mod
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · harnu-probe
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /harnu-probe ⎿ harnu-probe: {"sessionId":"preview-session","pluginsRegisteredAfterProbe":[],"tokenReadable":false} ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Harnu

Harnu — Manage every Claude Code session in one place.

A desktop app that turns the sprawl of running and idle Claude Code sessions into a single browsable workspace.

Not affiliated with Anthropic. Harnu is an independent, unofficial project. It is not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic, PBC.

What it is

Harnu is a cross-platform Electron + Vue 3 desktop app. It reads ~/.claude/projects/, where Claude Code persists every session as JSONL, and renders it as a sidebar of folders with their sessions underneath; folders that are worktrees of the same repo are grouped together. Click a past session to resume it via claude --resume <uuid>; click "new session" to spawn claude in the chosen folder's cwd. Renames issued inside Claude (via /rename) propagate back to the sidebar live through a filesystem watcher. No daemon, no cloud, no account — the app is a faithful UI over files that already exist on disk.

Features

  • Sessions in one place — resume any past Claude Code session or start a new one, grouped by folder and git worktree, with live status (working / needs input / stuck / idle) right in the sidebar.
  • Approval Inbox — every action an agent takes through Harnu's control server surfaces in one rail for your say-so instead of scattered terminal prompts (Claude Code's own permission prompts join it only if you switch the opt-in Interceptor to Active in Settings → Interceptor; it ships in log-only Shadow mode, which never changes a session); grant a whole batch of actions at once or allow a verb durably per folder.
  • Git worktrees — spin up an isolated worktree for a task straight from Harnu, seeded via a repo's own WORKTREE.md manifest so it's usable immediately instead of a bare checkout.
  • Project memory — a per-repo .harnu/memory/ spotlight (hot state, decisions, roadmap) that every session and worktree shares, browsable from a built-in pane.
  • Roadmap board — a kanban for the project's backlog with agent-authored cards, a dispatch manifest gate, and configurable model routing per card kind.
  • Claude Boot — per-folder launch options (model, effort, flags, custom Claude-compatible endpoints).
  • Usage dashboard — plan usage at a glance plus a full history/cost breakdown across sessions.
  • MCP control server — lets a session act on the fleet itself (create sessions/worktrees, read project memory, manage the roadmap) through a typed tool catalog, gated by the Approval Inbox.

See the user guide for how to use all of this — start with Getting started if this is your first time running Harnu.

Status

Still in alpha — no signing, no notarization. The binaries are unsigned on every platform. Expect Gatekeeper (macOS) and SmartScreen (Windows) to block the first launch. See the platform notes below for the bypass per OS.

The data plane, terminal pipeline, shortcuts, and command palette are wired. Auto-update applies silently on AppImage; unsigned macOS/Windows builds get an in-app "Update available" toast linking to the release page instead (the OS won't let an unsigned build apply an update silently). The .deb doesn't auto-update at all.

Requirements

  • Claude Code: the claude CLI installed, on your PATH, and logged in. Harnu runs your own claude; it does not bundle or replace it.
  • git, for the worktree features.
  • GitHub CLI (gh), optional, logged in, for the pull-request features (PR Stack, Review, Cleanup).

Install

Download the build for your platform from the releases page.

Linux

sudo dpkg -i harnu_*_amd64.deb
# or
chmod +x Harnu-*.AppImage
./Harnu-*.AppImage

macOS

Drag the .dmg to Applications. On first launch Gatekeeper will refuse to open it. Either:

xattr -d com.apple.quarantine "/Applications/Harnu.app"

Or right-click the app in Finder and choose Open — macOS then offers a one-time exception.

Windows

Run the .exe installer. SmartScreen will show "Windows protected your PC". Click More info → Run anyway to proceed. Subsequent launches are clean.

Network activity

Harnu is a local-first app — there is no Harnu backend, no account, and no telemetry, analytics, or crash reporting. Every outbound call Harnu (or a tool it runs for you) makes is listed below. Anything that reaches Anthropic goes through your own claude CLI and your own login; Harnu holds no Anthropic credentials of its own. Git and GitHub features run through your own git and gh, so the hosts they contact are whatever your remotes and gh login point at.

Automatic (no action from you):

DestinationPurposeWhen
status.claude.comShows the current Claude service incident state.Always: every 60 s while a window is focused, every 5 min in the background.
raw.githubusercontent.comFetches the official Claude Code changelog and notifies you of new versions.Always: every 30 min, whether or not Settings is open.
Anthropic, via claude -p /usageReads your plan-usage meters for the footer. Sends no prompt content.Immediately on focus, then every 90 s while a window is focused.
GitHub Releases (github.com / objects.githubusercontent.com)Update check. AppImage builds download and apply silently; macOS/Windows builds only show a toast linking to the release page. The .deb is managed by apt and is not replaced in place.Packaged builds only: 5 s after launch, then hourly.
Your git remotes (git ls-remote) and GitHub via your gh (gh pr list)Cleanup (Reaper) scan: which branches and worktrees are merged and safe to clean up. Read-only.Default on, hourly. Change the interval or turn it off in Settings → Cleanup.
GitHub via your gh (gh pr list)Pull-request state for PRs linked to an open Mission.While a Mission with linked PRs is open.

On your action:

DestinationPurposeWhen
Your git remote (git fetch)Fetches a base branch when you create a worktree, and a PR head for the Review pane.When you create a worktree or open a PR in Review.
Your git remote (git push --delete)Cleanup deletes a merged remote branch.Only after you confirm a sweep; can be disabled with neverDeleteRemote.
GitHub via your ghPR Stack, Review pane (including submitting a review), and Cleanup PR lookups.When you open those views or click.
Anthropic, via your claude CLIUsage history Ask sends your question plus aggregated usage figures.When you submit a question.
Anthropic (or your custom endpoint), via your claude CLIThe Claude Code sessions themselves, including Scheduler workers you created and enabled.When you start or resume a session, or a worker you enabled runs.
cdn.jsdelivr.net and huggingface.coOne-time download of the offline voice engine (about 120 MB for the default voice).Only when you click install in Settings → Voice. Afterwards it runs offline.

Opt-in:

DestinationPurposeWhen
Anthropic, via your claude CLIAuto-name sends the first user prompt of a new session to Haiku to name it. Off by default.Once per new session, only if enabled in Settings → Intelligence.
The ntfy topic or webhook URL you configureRemote push notifications. The payload is the notification title and body, which can include the session name or summary. Off by default.Only after you add a channel; goes only to the URL you enter.

Your configuration:

DestinationPurposeWhen
The custom ANTHROPIC_BASE_URL endpoint you setPoints your claude sessions at an alternate Claude-compatible server (Settings → Endpoints). Harnu only sets the environment variable; the CLI makes the call.Only if you set one.

Everything else — reading ~/.claude/projects/, spawning claude, the terminal pipeline — stays on your machine.

Stored credentials

Custom-endpoint auth tokens (for alternate Claude-compatible endpoints) are stored in plaintext in claude-boot.json inside the app's userData directory. They are not encrypted at rest — treat that file as sensitive.

Development

npm install
npm run dev

npm install rebuilds node-pty for the Electron ABI via the postinstall hook. npm run dev starts electron-vite with HMR for the main, preload, and renderer processes.

npm run build       # full production build (typecheck + electron-vite build)
npm run typecheck   # both: typecheck:node and typecheck:web
npm run build:linux # electron-builder for .deb + AppImage
npm run build:mac   # electron-builder for .dmg
npm run build:win   # electron-builder for NSIS .exe

Live-verify a change in the real app (without disturbing a running instance)

To prove a change works end-to-end against the actual app — even while another Harnu (an AppImage or npm run dev) is already running — launch a second, isolated instance (its own --user-data-dir bypasses the single-instance lock; an alternate --remote-debugging-port avoids CDP collisions), drive it over the DevTools Protocol, and inspect what it wrote to disk. Full reproducible recipe (including the is.dev renderer-URL gotcha and safe teardown) is in docs/dev/live-verify-second-instance.md.

Architecture

ARCHITECTURE.md maps the three Electron processes and where code lives. design.md is the visual system, the single source of truth for tokens and components. CHANGELOG.md is the release history, and docs/ holds the user guide, ADRs and engineering notes. CLAUDE.md is the same contract written for AI coding agents working in this repo.

Project layout

src/main/        — Electron main process (PTY, watcher, IPC, MCP control server)
src/preload/     — typed contextBridge API
src/renderer/    — Vue 3 app (sidebar, terminal, dialogs, palette)
tests/           — Vitest unit tests and Playwright e2e
docs/            — user guide, ADRs, lessons, specs
scripts/         — CI gates and generators
build/           — packaging assets (icons, entitlements)
resources/       — runtime assets shipped with the app (icon, bundled skills, templates)

Contributing

Contributions are welcome. Start with CONTRIBUTING.md: it covers setup, the conventions the CI gates enforce, and what a pull request needs. Everyone taking part follows the Code of Conduct. Report security issues privately, as described in SECURITY.md.

The project was developed in a private repository before its public release. That history was not carried over; this repository starts from a single initial commit, and the design decisions it holds are recorded in the ADRs and the changelog.

License

MIT. See LICENSE. Third-party software and assets bundled with the app are listed in THIRD-PARTY-NOTICES.md.

Source 1 files
hooks/register.ts 65 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// Test-only. /harnu-probe answers from `command.run`, so a `claude -p "/harnu-probe"` run makes
4// no model request (smoke C4). It prints, as JSON, what it observed.
5//
6// P1W3 additions, for the handshake suite: when HARNU_PROBE_OUT names a file the probe writes
7// what it saw there (hook payload shapes and the `session.end` budget). It never reads or prints
8// a token; it only records field NAMES of the classic payload and a few ids.
9
10const classic: { source: string; sessionId: string; keys: string[] }[] = []
11let endBudgetMs: number | null = null
12
13async function saveNotes($: EngineInterface): Promise<void> {
14  const out = await $.env.get('HARNU_PROBE_OUT')
15  if (typeof out !== 'string' || out === '') return
16  await $.fs.write(out, JSON.stringify({ classic, endBudgetMs }))
17}
18
19export const register: Register = (on) => {
20  const seen: string[] = []
21
22  // Sees every hooks module admitted AFTER this one: the load-order probe (AC-P1W2-17).
23  on('plugin.register', async (_$, e, next) => {
24    seen.push(e.name)
25    return next(e)
26  })
27
28  on('session.start', async ($, e, next) => {
29    await $.command.register({ name: 'harnu-probe', description: 'Report what the probe observed' })
30    return next(e)
31  })
32
33  on('classic.SessionStart', async ($, e, next) => {
34    classic.push({ source: e.source, sessionId: e.session_id, keys: Object.keys(e).sort() })
35    try {
36      await saveNotes($)
37    } catch {
38      // best effort: the probe must never break the run
39    }
40    return next(e)
41  })
42
43  on('session.end', async ($, e, next) => {
44    endBudgetMs = next.budget.remainingMs
45    try {
46      await saveNotes($)
47    } catch {
48      // best effort: the probe must never break the run
49    }
50    return next(e)
51  })
52
53  on('command.run', { command: 'harnu-probe' }, async ($) => {
54    const token = await $.env.get('HARNU_SPAWN_TOKEN')
55    return {
56      text: JSON.stringify({
57        sessionId: await $.session.id(),
58        pluginsRegisteredAfterProbe: seen,
59        // Presence only: the token itself is never printed or logged.
60        tokenReadable: typeof token === 'string' && token.length > 0
61      })
62    }
63  })
64}
65