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 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, OpenAI Codex CLI, and opencode are supported, all through the same agent-agnosticcasperCLI.
- 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). - Agent integrations — works with Claude Code, OpenAI Codex CLI, and opencode, and launches none of them. Casper detects whether each agent's Casper integration is installed and current, and shows a quiet, dismissible reminder in the sidebar when one needs attention; it never writes another tool's configuration. See Coding agents.
- 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. Each terminal remembers the font size you set with ⌘+ / ⌘- / ⌘0 and restores it on relaunch. - Open in Editor — a title-bar split button opens the workspace's worktree in Visual Studio Code, IntelliJ IDEA, or Xcode; each workspace remembers the editor it was last opened with.
- Per-workspace port reservation — a contiguous block of 10 ports per
workspace, injected as
CASPER_PORTin worktree workspaces only, so the same app can run once per worktree without collisions. The repository's main working tree gets noCASPER_PORTand keeps the project's default ports. - Native & lean — prefers built-in macOS frameworks; only five external dependencies (libghostty, swift-argument-parser, libgit2, HighlightSwift for diff syntax highlighting, and Sparkle for auto-update); arm64-only.
Casper supports three coding agents — Claude Code, OpenAI Codex CLI, and opencode — and treats them alike: agent state, progress, notifications and the info panel all work the same whichever one you use. Casper never launches an agent for you; you start yours in a Casper terminal yourself.
Agents talk to Casper through the casper CLI (see CLI). You can call
those commands by hand, but the usual route is a small integration plugin
installed into the agent, which wires the agent's own lifecycle to them so the
sidebar badge, the progress bar and the notification dot work without you
instrumenting anything. Each agent installs that plugin with its own installer:
Casper never writes another tool's configuration. All Casper does is detect
what an installer left behind.
Casper asks one question per agent, for the whole app rather than per workspace: do you have that agent's CLI, and is its Casper integration installed and current? An agent whose CLI you don't have is ignored entirely. No reminder for it ever appears, and Casper reads nothing on your disk to work that out.
It asks once at launch and rechecks every few seconds from then on, so you can install or update a plugin in a Casper terminal and watch the line retire itself a moment later — no relaunch, nothing to click. Installing the agent's own CLI while Casper is running is the one change it won't notice: restart Casper and the agent appears.
When there is something to say, one quiet line per agent appears in the sidebar just above Add Folder…; when there isn't, nothing is drawn at all:
- "… integration not installed" — you have the agent, but not its Casper integration. A plugin you have explicitly switched off says the same thing: none of its hooks run, so an install that is disabled is functionally absent.
- "… integration is outdated (0.1.0)" — installed, but older than the plugin version this build of Casper expects. The version in brackets is the one you currently have.
- "Codex integration needs approval" — informational rather than a fault; see below.
Click a line to open that agent's integration guide, or the × beside it to dismiss it for good. A dismissal silences that one problem, not the agent: once the integration reports healthy the dismissal is retired, so if it later breaks or you uninstall it, Casper tells you again.
Casper errs towards silence throughout. An integration whose version it cannot read counts as current, and an integration recorded in several places is judged by the newest record — a reminder you did not need is worse than one you missed.
Codex hashes command hooks it did not install itself and refuses to run them
until you review and approve them with /hooks in its TUI. A Codex integration
can therefore be installed, current, and completely inert, and nothing on disk
records whether you have approved it — so rather than guess, Casper always
states the caveat while your Codex integration is otherwise healthy. That line
dismisses under its own key, independently of the other reminders.
Codex detection is also built from Codex's published documentation and has never been verified against a real Codex install. If Casper reports your Codex integration as missing when you know it is there, that is the likeliest explanation — dismiss the line.
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). The casper CLI
needs no installation: Casper injects it into the PATH of every terminal it
opens, so agents and shells running inside a workspace can call it directly.
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.
| Shortcut | Action |
|---|---|
⌘O |
Add Folder… — open a repository as a new Space |
⌘D |
Split Right |
⌘⇧D |
Split Down |
⌘1–⌘9 |
Switch to the sidebar's 1st–9th workspace |
⌘C |
Copy the terminal selection |
⌘V |
Paste into the terminal |
⌘A |
Select all in the terminal |
⌘+/⌘- |
Grow / shrink the focused terminal's font |
⌘0 |
Reset the focused terminal's font size |
Holding ⌘ for a moment reveals the ⌘1–⌘9 number hints in the sidebar.
The rest of this document is for contributors who want to build Casper locally.
- Xcode 26 or later (full) — required to build at all (the project uses
Swift 6.2 isolated conformances) and to 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 icon # regenerate Packaging/AppIcon/AppIcon.icns from icon.svg (needs resvg)
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
both macos-15 (the deployment target's floor) and macos-26
(.github/workflows/ci.yml) — some AppKit layout
behaviour differs between the two, so a single runner would only ever show it as
a failure on the other machine. 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 binary — app + CLI] --> UI[CasperUI]
App --> CLI[CasperCLI]
UI --> Core[CasperCore]
UI --> Git[CasperGit]
UI --> Ghostty[CasperGhostty]
UI --> Agents[CasperAgents]
CLI --> Core
CLI --> Agents
Agents --> Core
Ghostty --> Core
Ghostty --> GK[GhosttyKit / libghostty]
Core --> Git
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 info set --message "## App ready
- API: <http://localhost:8080>"
casper info set --file docs/endpoints.md # read the Markdown message from a file
printf '## App ready\n' | casper info set # or read it from stdin
printf '## App ready\n' | casper info set - # same, with an explicit '-' marker
casper info clear # empty the panel and hide its button
casper terminal new # open a terminal (split below)
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 new feature/x --base main --command "claude"
casper workspace delete # destroy a workspace (worktree + branch)
casper run [name] # run a named .casper.json command in a split (defaults to 'run')casper workspace new <branch> takes the branch name as a positional argument;
--base <ref> forks from a ref other than the space's base branch, and
--command <cmd> seeds the workspace's first terminal with a command to run.
casper terminal new takes the same --command <cmd> to seed the new split,
plus --working-dir <path> to start it somewhere other than the workspace's
worktree.
The browser panel doubles as an automation surface, so a coding agent can drive and inspect the page it just changed:
casper browser load https://localhost:8080 # navigate without opening the panel
casper browser screenshot --out out.png # PNG of the page (--width/--height/--url)
casper browser content # dump the page's HTML
casper browser url # print the page's current URL
casper browser eval "document.title" # evaluate JavaScript in the page
casper browser click "button.submit" # click the first matching element
casper browser type "input[name=q]" casper # type into the first matching element
casper browser key Enter # dispatch a keydown/keyup to the page
casper browser console # captured console output + uncaught errors (--level)
casper browser wait ".ready" # block until a selector holds (or --js <expr>)
casper browser reload # reload the page
casper browser scroll-down # also scroll-up / scroll-top / scroll-bottomEvery workspace-scoped command accepts --workspace <id-or-name> to target a
workspace other than the current one. The one exception is workspace current,
which reports the terminal's own workspace from $CASPER_WORKSPACE_ID and takes
no target. Commands talk to the running app over a Unix domain socket named by
$CASPER_CONTROL_SOCKET, injected per terminal alongside $CASPER_WORKSPACE_ID
— and, in worktree workspaces only, $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. Ids are printed in
lowercase, and --workspace matches an id in either case. workspace delete is
destructive (it removes the worktree folder and its branch) and refuses the
primary workspace.
Casper installs and serves no agent hooks of its own: 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 per-agent integration plugin is only a
convenience on top — it wires the agent's lifecycle to exactly these commands.
The info panel keeps only the latest casper info set message and never
persists it across app restarts; it's reached by hovering or clicking the info
button next to the workspace's branch/space title. A message is capped at 256
KB; --message and --file together are an error; and a bare casper info set
at an interactive terminal errors instead of waiting on stdin (pipe, redirect,
or the explicit - marker all still work).
Clicking a link in the message opens it in the workspace's own browser panel, since a published endpoint is almost always local; Command-clicking it opens the same link in the system's default browser instead.
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.