A native macOS menu bar control deck for your local dev commands and background daemons.
Launch, stop, and monitor local dev commands and long-running daemons
(colima, minikube, just, kubectl port-forward, …) — without the chore of juggling terminals.
Swift + SwiftUI/AppKit ·
NSStatusItem+NSPopover· sandbox-free ·LSUIElement(menu bar only, no Dock icon).
- Menu bar deck on every display: click the icon → a minimalist popover with your commands, daemons, and chains; run/stop buttons and status indicators (grey / yellow spinner / green daemon / red).
- Collapsible sections with an active counter; the collapse state is remembered across launches.
- Main window: command editor, chain builder (drag-and-drop ordering), live logs.
- Chains — sequential execution, stop on error, the failed step highlighted.
- sudo commands — via the native macOS password dialog (
osascript … with administrator privileges). - Memory freeing — gracefully quit memory-hungry GUI apps before a heavy build and
relaunch them afterwards (for a memory-hungry
just dev-build). - Memory header — RAM (used/total/%), swap, color by pressure; auto-refreshes once a second.
- Cleanup page — where the colima disk goes (
docker system dffor the VM and for the docker inside the minikube node) with one confirm-first button per reclaimable category, and a colima restart for when the VM's page cache has eaten the Mac. The popover points at it past 85%. - Proxy Manager — share this Mac's VPN egress with another machine over the LAN: a built-in
HTTP proxy listener announced over Bonjour, and a per-command "route through the LAN proxy"
flag on the client side. Works out of the box; an alternative
gostengine covers SOCKS clients (see Requirements). - Remote proxy (VDS over SSH) — no second Mac needed: route flagged commands through a VDS
reachable over SSH. Nothing runs on the VDS but
sshd— DevDeck holds anssh -N -Dtunnel plus a local bridge and presents it as an ordinaryhttp://127.0.0.1proxy. See Remote proxy over SSH. - Browser via proxy — one click (in the popover or on the Proxy page) opens a separate Chrome
window that egresses through the active proxy — LAN or remote — with its own profile and without
touching your default browser. This is what makes browser logins like Claude Code's
/loginwork: the page loads through the proxy while thelocalhostcallback stays direct. - Diagnostics — a file log + crash reports; the "Log" button reveals
devdeck.login Finder. - JSON config, editable both by hand and from the UI; external edits are picked up by a FileWatcher, broken JSON → an error in the UI while the last valid version is kept in memory.
- Bilingual UI (EN / RU) — switch the interface language live in Settings, no restart required.
- In-app auto-update (Sparkle) — get new versions automatically, or see when one is available.
brew install --cask proshik/tap/devdeckThis adds the proshik/tap tap automatically and installs
the latest release. The icon appears in the menu bar (no Dock icon — it's an LSUIElement app).
-
Homebrew 6.0+ asks you to trust a third-party tap once. If you see "untrusted tap", run
brew trust proshik/tapand re-run the install. -
First launch — Gatekeeper. DevDeck is not notarized (free distribution), so Homebrew quarantines the download and macOS blocks the first launch. Clear it once:
xattr -dr com.apple.quarantine "$(brew --prefix)/Caskroom/devdeck"/*/DevDeck.app
(or right-click
DevDeck.app→ Open → Open). You only do this once.
Updates are delivered in-app via Sparkle — no brew upgrade needed (the cask is
auto_updates). In Settings → Updates:
- Automatic on → new versions download and install themselves;
- Automatic off → the popover shows a small ⤓ indicator with "current → latest (N behind)"; click it to update.
Sparkle-installed updates are not quarantined, so the first-launch step above is never repeated.
brew uninstall --cask proshik/tap/devdeck
brew untap proshik/tap # optional: remove the tap tooGrab the .dmg from Releases and follow
Installing on another machine below.
- macOS 15.0 (Sequoia)+ — the target minimum (deployment target); also runs on macOS 26 (Tahoe).
It can't go below 14.0 — the code uses macOS 14+ API (
@Observable,.focusEffectDisabled()). - Xcode 16+ to build.
- Optional:
just— for the short commands in thejustfile. - Optional:
gost(brew install gost) — only for the gost engine of the proxy share (the Proxy page), i.e. when a peer needs SOCKS. The default built-in engine serves HTTP (CONNECT) with no external dependency — enough for every DevDeck client and thedphelper. DevDeck looks for gost at/opt/homebrew/bin/gostor/usr/local/bin/gost; with the gost engine selected and no binary the share can't start (the editor shows a warning).
- Language: Swift, UI in SwiftUI + AppKit (
NSStatusItem,NSPopover). - Build: an Xcode project (
DevDeck.xcodeproj) — produces the.appbundle,Info.plist, icon, and live previews out of the box; the path to eventual distribution (Mac App Store / notarization). Info.plist:LSUIElement = true(no Dock icon, menu bar only). No sandbox — it needs to launch external processes and reach arbitrary working directories.- One third-party dependency: Sparkle (in-app updates), pulled in via SwiftPM. Nothing else — no networking, UI or utility libraries.
With Xcode: open DevDeck.xcodeproj, pick the DevDeck scheme, ⌘R.
From the terminal:
xcodebuild build -project DevDeck.xcodeproj -scheme DevDeck \
-configuration Debug -derivedDataPath build/dd
open build/dd/Build/Products/Debug/DevDeck.appWith just: just run
⌘U in Xcode, or:
xcodebuild test -project DevDeck.xcodeproj -scheme DevDeck -destination 'platform=macOS'just test. The unit tests exercise the ProcessManager state machine and chains on a fake
runner (no real processes are launched) + config round-trip, the ring buffer, memory formatting, and more.
File: ~/Library/Application Support/DevDeck/config.json (copied from the bundled
default-config.json on first launch). You can edit it by hand — changes are picked up automatically.
When there is no second Mac to share from, egress through a VDS reachable over SSH — a server in
a region where the services you need are available. Nothing is installed on the VDS: it needs only
sshd. DevDeck holds an ssh -N -D dynamic-SOCKS tunnel and a local bridge (the built-in
engine dialing through that SOCKS), presenting the whole thing as an ordinary
http://127.0.0.1:<port> proxy. Hostnames are resolved on the VDS, not locally.
DevDeck runs ssh as a background daemon — with no terminal attached. Anything ssh would
normally ask a human cannot be answered, so it must already be settled:
- Key-based auth, no passphrase prompt. A password login, or a key whose passphrase is not in
the agent, leaves the tunnel dead on arrival. Use a key and make sure the agent holds it
(
ssh-add -l); on macOS,UseKeychain yesin~/.ssh/configmakes that survive reboots. - The host key must already be trusted. The first connection to an unknown host asks
"Are you sure you want to continue connecting?" — nobody is there to type
yes. AllowTcpForwardingmust be on at the VDS (sshd's default, but hardened images disable it). Without it the tunnel connects and then forwards nothing, which looks like "everything is up but nothing works".
One check covers all three — run it in a terminal once. It must finish instantly, printing nothing and asking nothing:
ssh <your-host> true # silence = ready; any prompt = fix it before adding the proxyNo software is installed on the VDS and no root is needed there — an ordinary account with sshd
is the whole requirement.
- Proxy page → Remote proxies (SSH) → Add remote proxy… — give it a name and an SSH
destination (a host from
~/.ssh/config, oruser@host). The local HTTP port (18888) and SOCKS port (1080) have sane defaults. DevDeck creates a regular, editablessh -N -D …daemon command and links it — add-J jumphostor other options by editing that command later. - Select it in the list. DevDeck holds the tunnel and the bridge up (with watchdog restarts, and across app restarts) while it stays selected. The check button shows the VDS's public IP — proof traffic egresses there.
- Flag a command with "Route through the active proxy", or use the
dphelper from any terminal —dp claude. - Browser logins (
/loginin Claude Code): click Browser via proxy. It opens a separate Chrome window that egresses through the proxy, with its own profile, without touching your default browser. Paste the printed login URL there; thelocalhostcallback stays direct and reaches the waiting CLI. A real Chrome means Google SSO and passkeys work normally.
The
dpsnippet changed for this release (it now honors a network-independent scope). If you pasted an older one, re-copy it from Proxy page → terminal helper.
The tunnel is an ordinary daemon in the deck, so it fails visibly: its row goes red and the
watchdog keeps retrying, and the proxy stays unusable (a flagged command fails loudly rather than
going direct). What the log — "Log" button → devdeck.log — will tell you:
| Symptom | Cause |
|---|---|
| Tunnel dies immediately, over and over | ssh wants something a daemon can't answer — see Before you start |
| Tunnel stays up, the check button fails | AllowTcpForwarding no on the VDS, or the SOCKS port is taken locally |
| "Port 1080 is occupied" panel | something else holds the port — the panel offers to kill it, or change the port on the proxy |
To see the raw error, run the tunnel command by hand once: open it from the deck, or paste its
ssh -N -D … line into a terminal — there ssh can finally tell you what it wanted.
The UI ships in English and Russian. Switch it live under Settings → Language in the main window — the whole interface updates instantly, with no app restart. On first launch the language follows your system preference (Russian → Russian, otherwise English); your choice is then remembered.
devdeck/
├── DevDeck.xcodeproj
├── DevDeck/
│ ├── DevDeckApp.swift / AppDelegate.swift # @main, LSUIElement, exit dialog, crash handlers
│ ├── Models/ # Command, Chain, Config, AppRef (Codable)
│ ├── Store/ # CommandStore (JSON load/save), ConfigCodec, FileWatcher
│ ├── Process/ # CommandRunner (protocol), Zsh/Sudo runners, ProcessManager (@Observable),
│ │ # StreamingProcess, RingBuffer, AppController (quit/relaunch GUI apps)
│ ├── MenuBar/ # MenuBarController (NSStatusItem+NSPopover), PopoverView, TrayIcon, StatusIndicator
│ ├── MainWindow/ # MainWindowView, CommandEditorView, ChainEditorView, LogView, SettingsView
│ ├── Localization/ # LocalizationManager (live language switch) + L10n catalog (EN/RU)
│ ├── Diagnostics/ # DiagnosticLog (file log + crashes), SystemMemory (RAM/swap/pressure)
│ ├── Cleanup/ # docker system df probe, cleanup commands, CleanupModel
│ └── Resources/ # Assets.xcassets (tray glyph + AppIcon), default-config.json
└── DevDeckTests/ # state machine, chains, store, codec, memory, runners
Architecture details are in CLAUDE.md; the per-item plan and status are in
docs/PLAN.md.
./scripts/build-dmg.sh # or: just dmgThe script: builds the Release .app, applies an ad-hoc signature (a stable code signature without
a Developer ID), stages it together with a symlink to /Applications (drag-to-install), and creates a
compressed build/DevDeck-<version>.dmg image via hdiutil (no external dependencies).
Want a "pretty" dmg with a background and icon layout? Install
create-dmg(brew install create-dmg) and replace thehdiutilcall in the script — the staging layout is already compatible.
The app is not signed with a Developer ID and not notarized, so Gatekeeper will block the first launch ("DevDeck can't be opened because the developer cannot be verified" / "is damaged"). This is expected. The target Mac must be on macOS 15 (Sequoia)+.
- Copy
DevDeck-<version>.dmgto the machine, open it, drag DevDeck intoApplications. - Clear the quarantine one of these ways:
- Right-click
DevDeck.app→ "Open" → "Open" (the dialog remembers the permission); or - in the terminal:
xattr -dr com.apple.quarantine /Applications/DevDeck.app, then launch; or - launch it, get the rejection, then System Settings → Privacy & Security → "Open Anyway".
- Right-click
- The icon appears in the menu bar (it's not in the Dock — that's
LSUIElement).
The quarantine attribute is set only when downloading via a browser/AirDrop. If you move the dmg over
scp/a flash drive, the quarantine-clearing step may not be needed.
The deployment target is 15.0 (Sequoia); the app runs on macOS 15 and 26. Change it via
MACOSX_DEPLOYMENT_TARGET in DevDeck.xcodeproj. The lower bound is 14.0: the code uses macOS 14+
API (Observation @Observable, .focusEffectDisabled()) and can't go below without changes.
After changing the target — run the tests.
MIT — see LICENSE.
{ "commands": [ { "id": "UUID", "name": "just dev-build", "command": "just dev-build", "workingDirectory": "/path/to/project", // optional "isDaemon": false, // a long-running daemon? "needsSudo": false, // run with admin rights? "env": { "CARGO_BUILD_JOBS": "3" } // optional extra env } ], "chains": [ { "id": "UUID", "name": "Full restart", "commandIDs": ["UUID", "..."], "stopOnError": true } ] }