Drydock is a macOS desktop application for managing local Git repositories and
the claude CLI sessions you run
against them. Each repository gets a sidebar entry with live branch and
dirty-state indicators; each Claude session runs in an embedded terminal inside
the app — a real Ghostty terminal surface, not a
reimplementation and with no external multiplexer (tmux, screen) involved.
The goal is a single window where you can see every repo you work in, start or
resume a claude session per repo in its own tab, and keep an eye on session
activity, Git status, and your changes without leaving the app.
- Repository sidebar — add/remove local Git repositories; live branch and dirty indicators; open a repo in Finder or your external editor.
- Embedded terminal sessions — run the real
claudeCLI in embedded Ghostty terminal tabs, one per session, with session activity surfaced in the UI. - Git & GitHub awareness — repository status, review, and search surfaces built around the repos you register.
- Persistent state — registered repositories and window layout persist across restarts.
- Remote repositories over SSH — register a repo on a remote host via
ambient SSH auth; sessions run
claudeon the host, the sidebar shows live indicators. Remote sessions show a neutral 'session ended — resume to reconnect' state on exit; worktrees, diffs, Explorer/Review, and activity badges are unavailable. Requiresgitandclaudeon the host's non-interactive PATH, a POSIX login shell, and an already-accepted host key.
Drydock is macOS-only and runs natively on both Apple Silicon (arm64) and
Intel (x86_64).
Run ./scripts/verify-environment.sh to check most prerequisites automatically.
| Tool | Version | Notes |
|---|---|---|
| JDK | 26 | Gradle toolchain used to compile and run the app (sdk install java 26.0.1-tem). |
| JDK (for Gradle itself) | ≤ 24 | Gradle 8.11.1 cannot run on JDK 26; its launcher needs an older JDK. See below. |
| Gradle | 8.11.1 | Provided by the checked-in wrapper (./gradlew); no separate install. |
| zig | 0.15.2 exactly | Required to build vendored libghostty (brew install zig@0.15). Not 0.16.x. |
| Xcode | Full Xcode + Metal Toolchain | Needed to compile Ghostty's Metal shaders. Install the component once with xcodebuild -downloadComponent MetalToolchain. |
| git | recent 2.x | |
claude CLI |
recent | The session Drydock manages. |
Gradle on JDK 26. Gradle 8.11.1 throws on a JDK 26 JVM, so two JDKs are in play: an older one (verified with JDK 23) launches Gradle, and the JDK 26 toolchain compiles and runs the app. If your default
javais JDK 26, point Gradle at an older JDK for its launcher:export JAVA_HOME=~/.sdkman/candidates/java/23.0.1-tem export PATH="$JAVA_HOME/bin:$PATH"Gradle's toolchain support then auto-detects the installed JDK 26 for the app.
If you have jbang installed, you can run Drydock without installing a JDK, Gradle, or the native toolchain:
jbang drydock@btraceio/drydockUse the catalog alias (above), not the bare io.btrace:drydock:0.1.0
Maven coordinate: the alias carries the required JDK version and JVM flags,
which a plain coordinate cannot. If jbang launches on an older JVM — you'll see
InvalidModuleDescriptorException: Unsupported major.minor version 68.0 while
reading a JavaFX module — jbang could not provision JDK 26. Fix it with
jbang jdk install 26 (and jbang version --update if jbang is old), then
re-run with jbang --fresh drydock@btraceio/drydock.
jbang provisions a Temurin JDK 26 and JavaFX automatically. The app's classes
and the required native libraries (libghostty, libdrydockterminalhost, for
both Apple Silicon and Intel) ship inside the io.btrace:drydock Maven Central
jar; on first launch they are extracted to ~/Library/Caches/drydock/native/.
First launch downloads the JDK and dependencies, so it's slower than subsequent
runs. A few warnings on startup are benign and can be ignored: WARNING: Unknown module: javafx.graphics (JavaFX runs from the classpath, so the export flag is
a harmless no-op), a jbang/Gson reflective-final-field warning, and — only when
run without a display — CVDisplayLink error lines.
Limitations vs. the packaged app. A jbang launch is a plain JVM process, not a
signed/notarized .app bundle: no Finder double-click and no bundle identity
(the dock name and Bot-in-dock icon are still set). For the polished,
double-clickable app, build the .app with ./gradlew appImage. The dylib load
relies on Temurin's disable-library-validation entitlement; a non-Temurin JDK
may refuse to load them.
Ghostty is vendored as a pinned Git submodule, so initialize submodules first:
git submodule update --initThen compile, run tests, and launch the app:
./gradlew clean test run./gradlew run opens the Drydock window: a repository sidebar on the left and
the workspace (terminal tabs) on the right. Registered repositories and window
layout persist across restarts under
~/Library/Application Support/. Close the window (or Cmd+Q) to exit.
To build a self-contained runtime image (JDK 26 + JavaFX 26 + the app + native
libraries, for both macOS architectures) that runs without Gradle or a
configured JAVA_HOME:
./gradlew runtimeImage
build/image/bin/drydock.app/.dmg packaging is not implemented yet; build from source or use the
runtime image.
See docs/RELEASING.md for how releases are staged to Maven Central and finalized.
The design, milestone reports, and native-integration findings live in
docs/:
docs/implementation-plan.md— full design and scope.docs/native-integration.md— how libghostty is built, its C API, required frameworks, and lifecycle/threading constraints.docs/runtime-image.md— the jlink runtime image layout and launcher details.docs/architecture.md— architecture and open risks.
Native code lives at the edges: the AppKit host shim in
native-host/ (built by ./gradlew buildNativeHost) and the
FFM bindings under app/src/main/java/app/drydock/terminal/. Ghostty itself is
pinned at third_party/ghostty (tag v1.3.1) and built by
scripts/build-ghostty.sh — CPU-architecture selection stays isolated in the
native boundary package and must not leak into UI, Git, or persistence code.
Build the native pieces directly with:
./gradlew buildGhosttyNative # libghostty, both architectures
./gradlew buildNativeHost # the AppKit host shim, both architectures