Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dist/
.swiftpm/
PLAN.md
MEMORY.md
plans/
.claude/
.fuzz-progress
fuzz-crashes/
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ let package = Package(
.executableTarget(
name: "Perch",
dependencies: ["PerchCore"],
swiftSettings: swiftSettings
swiftSettings: swiftSettings,
linkerSettings: [.linkedLibrary("sqlite3")]
),
// Harness tooling — offline oracles for bug-bashing PerchCore. Not
// shipped in the app bundle (Makefile copies only Perch/perch-bridge).
Expand Down
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ and what they've **left behind**:
| 🐦 **Every session at a glance** | Live list of all Claude Code and Codex sessions — running / waiting / idle, last message, context gauge, red badge on any session that just ran something dangerous. |
| 🎫 **Token usage** | Today / 7-day / 30-day totals in the notch, rate-limit gauges with reset countdowns, and a full per-day / per-model / per-project dashboard (menu bar → **Token Usage…**). |
| 🌳 **Worktree housekeeping** | A read-only cross-project audit of the git worktrees agent sessions leave behind — classified `reclaimable` (clean, merged, stale), `review` (dirty or ahead of the default branch), `active` (a live session or recently touched), or `orphaned` — with disk sizes and a *Copy cleanup commands* button (menu bar → **Worktrees…**). Perch scores and reports; it never deletes. |
| 🪶 **Zero footprint** | No dependencies, no telemetry, and an auditable all-Swift codebase. If Perch dies, your agents don't even notice. |
| 🪶 **Zero footprint** | No third-party dependencies or telemetry, and an auditable all-Swift codebase using macOS's system SQLite for minimal local detection metadata. If Perch dies, your agents don't even notice. |

<details>
<summary><b>🧭 Footholds · 🌳 Worktrees · 📊 token dashboard screenshots</b></summary>
Expand Down Expand Up @@ -228,7 +228,8 @@ Option 2 — it's two commands.
Claude Code / Codex ──hooks──▶ perch-bridge ──unix socket──▶ Perch.app
(your terminal) (fire & forget, ├─ risk scoring
keeps all decisions ~10 ms, exits) ├─ notch card + notification
└─ sessions / tokens / score
├─ sessions / tokens / score
└─ minimal local SQLite record
```

Hooks invoke the bundled `perch-bridge`, which forwards each event over a
Expand All @@ -238,6 +239,14 @@ danger raises an OS notification and a notch card. In parallel, Perch tails
transcript/rollout files and validates liveness against `~/.claude/sessions`
pid files, so sessions started before Perch launched are covered too.

Accepted caution/danger detections also write compact metadata to
`~/Library/Application Support/Perch/detections.sqlite3` after the hook reply
and live-feed deduplication. The database retains 30 days and restores only the
past hour's posture after restart. It never stores commands, tool payloads,
paths, prompts, finding prose, decisions, or outcomes. The versioned,
read-only consumer contract is documented in
[Detection storage](docs/detection-storage.md).

One caveat: Claude's rate-limit gauges are fed by the statusline payload,
which only terminal `claude` sessions render — the Claude desktop app never
invokes it. Detection, sessions, and token totals work everywhere.
Expand All @@ -257,17 +266,23 @@ be audited, not trusted**:
fire-and-forget ~10 ms, and if Perch is wedged the hook gives up on its
own after 5 s — the agent always proceeds.
- **100% local detection, zero telemetry.** No analytics, no cloud detection
service — nothing Perch observes ever leaves your machine. The one network
service — nothing Perch observes ever leaves your machine. Accepted
caution/danger detections retain only minimal metadata in local SQLite for
30 days; there is no uploader. The one network
call in the codebase is the optional update check: an unauthenticated GET
to the GitHub releases API, on by default, toggleable from the menu bar
(**Check Automatically**), and zero network when off. Verify it yourself:
`grep -rn "URLSession\|NWConnection" Sources/` matches only
[`UpdateChecker.swift`](Sources/Perch/Model/UpdateChecker.swift).
- **The detector can't leak what it inspects.** Risk scoring is pure string
matching in-process; flagged commands are shown to you and written to your
local log, never sent anywhere.
- **No dependencies.** AppKit/SwiftUI/Foundation only. The supply-chain
surface is this repo — read it top to bottom.
- **The detector doesn't persist what it inspects.** Risk scoring is pure
string matching in-process. Commands and tool payloads may appear in the
live card, but SQLite stores only event/endpoint/tool identifiers, Perch
version, risk level, and stable finding codes. No commands, paths, prompts,
content, decisions, or outcomes are stored. See
[Detection storage](docs/detection-storage.md).
- **No third-party dependencies.** AppKit/SwiftUI/Foundation plus the SQLite
library shipped with macOS. The supply-chain surface is this repo and the
operating system — read the app top to bottom.
- **Config writes are surgical and reversible.** Installing hooks
parse-merges your `~/.claude/settings.json` / `~/.codex/hooks.json`
(your keys and hooks preserved), writes a timestamped backup, and replaces
Expand Down
9 changes: 9 additions & 0 deletions Sources/Perch/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let securityPosture = SecurityPosture()
let monitoringHealth = MonitoringHealth()
let notificationPreferences = NotificationPreferences()
private let detectionStore = DetectionStore()

private var socketServer: UnixSocketServer?
private var livenessMonitor: LivenessMonitor?
Expand Down Expand Up @@ -69,6 +70,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
sessionStore.riskFeed = riskFeed
sessionStore.usageStore = usageStore
sessionStore.securityPosture = securityPosture
sessionStore.detectionStore = detectionStore
detectionStore.start { [weak self] result in
guard case .success(let restored) = result else { return }
Task { @MainActor in
self?.securityPosture.hydrate(restored)
}
}

let notifier = Notifier(sessions: sessionStore, preferences: notificationPreferences)
self.notifier = notifier
Expand Down Expand Up @@ -213,6 +221,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillTerminate(_ notification: Notification) {
monitoringHealth.persistVerification()
socketServer?.stop()
detectionStore.close()
PerchLog.info("Perch terminated")
}

Expand Down
Loading