Casper is a native macOS app that embeds libghostty to give every Git worktree its own terminal workspace — built for developers running code agents. It tracks each agent's state and task progress, reserves network ports per workspace, and bundles a native browser and diff viewer.
Status: under active development and not yet ready for general use. All the core layers — the terminal engine, the Git worktree layer, and the
caspercontrol CLI — and the SwiftUI app (Space-grouped sidebar, linked worktrees, tmux-style split panes, browser, and diff viewer) are built and have passed a live GUI verification pass; polish is ongoing. Claude Code is the first supported agent.
- Worktree = workspace — each workspace maps to a Git worktree; creating one opens a plain Ghostty terminal in that worktree (no agent is auto-launched). ⌘-click a link in the terminal to open it in your browser.
- Agent state & progress — each workspace carries an agent state
(
working/blocked/idle/done/unknown/error) and acompleted / totaltodo progress bar, surfaced in the sidebar with pending-notification dots. State is inferred from terminal output by built-in detection (no hooks) and can also be set explicitly via thecasperCLI (see below). - Split-pane layout — tmux-style nested splits (one terminal per pane, no
tabs); a collapsible right-hand inspector offers a
WKWebViewbrowser and a native diff view per workspace. - Per-workspace port reservation — a contiguous block of 10 ports
(
CASPER_PORT) per workspace, so the same app can run once per worktree without collisions. - Native & lean — prefers built-in macOS frameworks; only four external dependencies (libghostty, swift-argument-parser, libgit2, and HighlightSwift for diff syntax highlighting); arm64-only.
Casper is distributed as a standalone Casper.app.
- Download the latest
Casper.apparchive from the Releases page. - Unzip it and move
Casper.appto your/Applicationsfolder. - Launch it like any other macOS app.
Requirements: macOS 15 or later, on Apple Silicon (arm64). On first launch, Casper wires up its code-agent integration for you — no manual setup.
Updates: Casper checks for new releases once a day and offers them through Casper ▸ Check for Updates…; nothing is installed without your say-so. Every update is verified against a signing key embedded in the app, so a tampered download is refused.
The rest of this document is for contributors who want to build Casper locally.
- Xcode (full) — required to build and run the tests; the Command Line Tools
alone cannot link XCTest. Select it with
sudo xcode-select -s /Applications/Xcode.app. - libgit2 and pkgconf —
brew install libgit2 pkgconf. CasperGit links libgit2 via pkg-config. - vendir —
brew install vendir. Carvel's file-vendoring tool, used to sync the pinned libghostty reference header (make vendor).
The first build downloads the pinned GhosttyKit.xcframework (~53 MB) from the
libghostty-spm release; subsequent builds reuse the extracted artifact.
git clone <repo-url> casper
cd casper
make vendor # sync the pinned libghostty header (once)
make build # compile
make test # run the test suiteCommon tasks are exposed through the Makefile:
make # debug build (default target)
make help # list available targets
make dev # recompile and launch the app under a per-branch dev session
make build # debug build
make test # run the full test suite
make all # build then test
make release # size-optimized release build (arm64)
make bundle # assemble a self-contained Casper.app (release binary + dylibs)
make dist # package Casper.app into a downloadable .zip + .sha256 + dSYM
make vendor # re-sync the pinned libghostty header via Carvel vendir
make clean # remove build artifactsmake bundle/make dist also need brew install dylibbundler to embed the
libgit2 dylib chain so the bundled Casper.app runs on a clean Mac.
make bundle compiles with -Osize, extracts the debug symbols to a
Casper.dSYM bundle next to Casper.app, and strips the shipped
executable. make dist publishes that dSYM as its own
Casper-<version>-arm64.dSYM.zip archive, so a crash report from a release can
still be symbolicated without shipping the symbols to every user.
make build assembles a minimal Casper-dev.app bundle around the debug
binary and signs it with a local Apple Development identity whenever one is
available in your keychain. This keeps the Screen Recording permission that the
debug-casper skill relies on (for screenshot capture) across rebuilds, instead
of macOS re-prompting after every recompile. The .app wrapper is required: a
bare signed executable never registers with macOS's privacy database (TCC) at
all, so only a real bundle can be granted the permission. Without an identity
everything still works — the bundle stays ad-hoc signed and you get the usual
re-prompt-on-rebuild behavior.
To create a free identity once (no paid Developer Program membership needed):
- Xcode → Settings → Accounts → + → Apple ID → sign in with any Apple ID.
- Select the resulting team → Manage Certificates… → + → Apple Development.
make buildpicks it up automatically from then on — no configuration required.
If security find-identity -v -p codesigning still reports 0 identities after
creating the certificate, the Apple WWDR intermediate certificate is likely
missing (codesign fails with "unable to build chain to self-signed root").
Download the current intermediate from
https://www.apple.com/certificateauthority/ (e.g. AppleWWDRCAG3.cer) and
install it with security add-certificates -k login.keychain-db AppleWWDRCAG3.cer.
See .superpowers/plans/screenshot-capture-permissions.md
for the full rationale.
Tests run on every push to main and every pull request via GitHub Actions on
macos-15 (.github/workflows/ci.yml). Tagging a
v* release builds and publishes Casper.app as a GitHub Release
(.github/workflows/release.yml), along with
the Sparkle appcast.xml feed the in-app updater reads. The release job signs
the archive with the SPARKLE_PRIVATE_KEY repository secret and fails if it is
missing — an unsigned feed would be rejected by every installed copy. See
.superpowers/plans/sparkle-auto-update.md.
Casper is a Swift Package split into focused modules so that the unstable libghostty API, the libgit2 layer, and agent specifics each stay isolated.
flowchart TD
App[Casper app + CLI] --> UI[CasperUI]
App --> Agents[CasperAgents]
UI --> Core[CasperCore]
UI --> Ghostty[CasperGhostty]
Agents --> Core
Core --> Git[CasperGit]
Ghostty --> GK[GhosttyKit / libghostty]
Git --> LG[libgit2]
| Module | Description |
|---|---|
CasperCore |
Models, session store, port allocator, control-channel protocol + socket (pure Swift) |
CasperGit |
In-house wrapper over libgit2 (worktrees, diff, status) |
CasperGhostty |
Embeds GhosttyKit; owns terminal surfaces and layout |
CasperAgents |
Per-surface environment injection (CASPER_WORKSPACE_ID, CASPER_CONTROL_SOCKET, …) |
CasperUI |
SwiftUI sidebar, chrome, diff, and browser views |
CasperCLI |
Domain subcommands, sharing the single app binary (swift-argument-parser) |
The app and CLI ship as one binary: an empty argv launches the GUI, while a recognized subcommand runs the CLI and exits.
casper is only reachable from inside a Casper-opened terminal, where the app
prepends its own binary directory to PATH. The CLI is organized by domain,
targeting the workspace behind the current terminal by default:
casper status set working # set the agent state (working|blocked|idle|done|unknown|error)
casper progress set --total 5 --current 2 --label "run tests"
casper progress clear
casper notify --message "needs review" # raise the attention flag + notify
casper terminal new # open a terminal (split right)
casper terminal list # list the workspace's terminals
casper terminal close <id> # close a terminal by id
casper browser open https://example.com # load a URL in the inspector browser
casper browser close # collapse the inspector if the browser is showing
casper diff open Sources/App/Main.swift # open the diff, scroll to a file
casper diff close # collapse the inspector if the diff is showing
casper workspace list # enumerate workspaces
casper workspace current # print the current workspace + path
casper workspace new feature/x # create a Git worktree workspace
casper workspace delete # destroy a workspace (worktree + branch)
casper run [name] # run a named .casper.json command in a split (defaults to 'run')Every workspace-scoped command accepts --workspace <id-or-name> to target a
workspace other than the current one. Commands talk to the running app over a
Unix domain socket named by $CASPER_CONTROL_SOCKET, injected per terminal
alongside $CASPER_WORKSPACE_ID and $CASPER_PORT.
Every command is machine-readable: on success it prints a JSON object (or array)
to stdout describing the affected workspace and any resulting state; on error
it prints {"error":"…"} to stderr and exits non-zero. workspace delete is
destructive (it removes the worktree folder and its branch) and refuses the
primary workspace.
Casper has no agent-hook integration: an agent reports its state by calling these
commands itself (e.g. casper status set working), so the surface is explicit
and agent-agnostic.
A repository can drop a .casper.json file at its root to tailor how Casper
treats its workspaces. Every key lives under workspace:
{
"workspace": {
"copyFiles": [".env", ".env.local"],
"scripts": {
"setup": "npm install",
"teardown": "docker compose down",
"run": "npm run dev",
"test": "npm test"
}
}
}-
copyFiles— patterns for untracked files seeded from the source worktree into a new workspace. It replaces the built-in.env/.env.localdefault;[]copies nothing. An invalid entry fails workspace creation before any Git mutation. -
scripts— shell commands bound to a workspace, each run in a visible terminal split. Two reserved keys are lifecycle hooks, run automatically and never invocable by hand:setupruns once, when the workspace is created (never on restart). A non-zero exit keeps its split open with the output and flags the workspace.teardownruns just before the workspace is destroyed (after the merge on the close path). Deletion proceeds whatever the outcome, bounded by a 30s timeout, so a broken cleanup script never traps you.
Every other key is a named command, launched on demand from the workspace's "Run Script" toolbar button and context menu, or with
casper run <name>. Its split closes automatically when the command succeeds (exit 0); on any non-zero exit the split stays open with a live shell so you can read the output and re-run.
The file is hand-edited (there is no settings UI) and re-read each time it is needed.
Casper is licensed under the Apache License 2.0.