A macOS menu bar app that captures a screenshot, uploads it to a remote host over SSH, and copies the resulting absolute remote path to the clipboard — ready to paste into a terminal running Claude Code (or anything else) over SSH.
Replicates this shell flow as a one-click menu bar action:
local="/tmp/beam_$(date +%s).png"
remote=".cache/screenshots/ss_$(date +%s).png"
ssh "$host" "mkdir -p ~/.cache/screenshots"
screencapture -i "$local"
scp "$local" "$host:$remote"
ssh "$host" "echo \$HOME/$remote" | pbcopy
rm "$local"- macOS Tahoe 26 (26.0+) — the project's deployment target is 26.0
- Xcode 26 with the Swift 6 toolchain
- Existing SSH keys /
~/.ssh/configset up for your remote hosts (the app bundles no credentials)
- Open
Beam.xcodeprojin Xcode 26. - Select the
Beamscheme and a My Mac destination. - In the target's Signing & Capabilities tab, set your Team (the project ships with team unset). Automatically manage signing is on by default.
- Build & run (⌘R). The app installs no Dock icon — look for the
camera.viewfinderglyph in the menu bar.
If you have just installed, you can build, run, and install the app from your terminal using the provided justfile:
just build: Compile a debug build (ad-hoc signed).just run: Build and run the debug version in-place.just install: Build the release version, copy it to your~/Applications/folder, and launch it.just stop: Stop any running instance of Beam.just clean: Clean up all build artifacts and thebuild/directory.just icons: Regenerate the app icons fromBeam.png(requiresimagemagick).
The first capture attempt prompts for Screen Recording permission. macOS
won't let screencapture do anything visible until that's granted; once
you've toggled it on in System Settings → Privacy & Security → Screen
Recording, the app will pick it up on the next click. On Tahoe the grant
may be re-confirmed periodically — the app rechecks CGPreflightScreenCaptureAccess()
on every invocation and deep-links you to the settings pane on denial.
Preferences… (⌘, from the menu) opens a list editor where you can add,
edit, and remove hosts. Each host has:
- Display name — what shows up in the menu
- SSH destination — an alias from
~/.ssh/config(e.g.my-server-alias) or a rawuser@hostname. The chevron button next to the field offers a picker populated from~/.ssh/config(Host directives + one level of Include globs; wildcard patterns like*are skipped). - Remote directory — where to drop the screenshot. Default
~/.cache/screenshots.~/means the remote$HOME; the~/prefix is stripped before scp/sftp transfer (since not all SFTP servers expand tildes) but is kept in the UI for clarity. - Filename template — controls the on-remote filename. Default
{date}/ss_{ts}.png. Tokens:{date}=YYYY-MM-DD,{ts}= unix seconds. The directory part of the template (e.g.{date}/) is created on the remote withmkdir -pbefore upload.
The config is persisted as JSON at
~/Library/Application Support/Beam/config.json.
Each host's menu expands into a submenu:
- Region —
screencapture -i, interactive marquee (the default and what the global hotkey triggers). - Full Screen — captures all displays without prompting.
- Window — interactive window-pick (
screencapture -iW). - Delayed… — 3 / 5 / 10 second countdown before an interactive region capture.
- Upload File… — skip screencapture; choose a local file via open panel and upload it as-is, preserving its extension.
The menu shows a Recent submenu of the last 10 successful upload paths. Click one to re-copy it to the clipboard. Useful if the upload's been clobbered by something else on your clipboard.
Beam tells ssh/scp to use OpenSSH connection sharing:
-o ControlMaster=auto
-o ControlPath=~/Library/Caches/Beam/cm/cm-%C
-o ControlPersist=600
The first capture to a host pays the full handshake; subsequent captures within 10 minutes reuse the same TCP connection and feel near-instant.
After installing the app, you'll find Upload to Beam in any file's
right-click → Services menu (and in the Services menu of any app's
menu bar). It uploads the selected files to the last-used host. Provided
by the standard NSServices mechanism — no separate extension target.
If the menu item doesn't appear right away, macOS rescans services on
login or when you run /System/Library/CoreServices/pbs -update.
The beam:// URL scheme accepts:
beam://prefs— open the Preferences windowbeam://capture— capture-and-upload for the last-used hostbeam://upload?path=/abs/path&host=<id-or-name>— upload an existing file;hostis optional (defaults to last-used)
Useful from launchers like Raycast, Alfred, or Stream Deck.
Default: ⌃⌥⌘S — triggers capture-and-upload for the last-used host.
Toggle and re-bind in Preferences: click the hotkey field, then press a
combo with at least one modifier. Esc cancels. The new chord is saved and
re-bound immediately. (Under the hood the field stores Carbon key codes
from <Carbon/HIToolbox/Events.h> in config.json.)
GUI-launched processes inherit a stripped environment from launchd, so
SSH_AUTH_SOCK and the PATH most users have in their shell rc files are
typically not set. ~/.ssh/config host aliases also won't resolve if ssh
can't find the agent. Beam works around this in ProcessRunner.swift:
- Start from
ProcessInfo.processInfo.environment. - Ensure
HOMEis set (sosshfinds~/.ssh/config) andPATHis sane. - If
SSH_AUTH_SOCKis unset, asklaunchctl getenv SSH_AUTH_SOCK. - Fall back to launching
/bin/zsh -lc 'printf %s "$SSH_AUTH_SOCK"'so a login shell's init scripts get a chance to set it. - Pass
ssh/scp-F ~/.ssh/configexplicitly to make config use deterministic.
If uploads fail silently after a build, check ~/Library/Logs/... or run
the same scp from the terminal — that's almost always where the problem
lives.
To make SSH_AUTH_SOCK available to GUI apps system-wide (recommended if
you use the agent for anything else), add this to your shell rc:
launchctl setenv SSH_AUTH_SOCK "$SSH_AUTH_SOCK"…or set it once at login via a LaunchAgent.
The project has Hardened Runtime enabled (required for notarization).
Sandboxing is explicitly off — the app shells out to /usr/sbin/screencapture,
/usr/bin/ssh, /usr/bin/scp, /bin/zsh, and /bin/launchctl, which the
sandbox would block.
If you re-sign or notarize the app, the Screen Recording grant in Privacy & Security is tied to the code-signing identity. Changing the identity invalidates the grant — macOS will ask again on the next capture. Same goes for changing the bundle identifier.
Beam/
├── main.swift NSApp bootstrap
├── AppDelegate.swift status item, menu, prefs window
├── Notifier.swift UNUserNotificationCenter wrapper
├── Models/Host.swift Codable host entry
├── Storage/
│ ├── HostsStore.swift JSON-backed @Published store
│ └── SSHConfigParser.swift ~/.ssh/config Host alias extractor
├── Capture/
│ ├── CaptureCoordinator.swift orchestrates capture → upload → copy
│ ├── CaptureKind.swift region/full/window/delayed enum
│ ├── FilenameTemplate.swift {date}/{ts} token expansion
│ ├── RemoteUploader.swift mkdir → scp → resolve, with ControlMaster
│ ├── ProcessRunner.swift Process wrapper, SSH_AUTH_SOCK propagation
│ └── ScreenRecordingPermission.swift CGPreflight/CGRequest helpers
├── UI/
│ ├── PreferencesView.swift SwiftUI Form, Liquid Glass by default
│ └── HotKeyRecorder.swift click-to-record hotkey field
├── HotKey/HotKey.swift Carbon RegisterEventHotKey wrapper
├── Info.plist LSUIElement = YES
└── Beam.entitlements sandbox off, hardened runtime via build setting