- Rust 85.3%
- Swift 9.1%
- Shell 5.6%
The host list was built once at startup; newly added ssh-config hosts didn't appear without restarting. The Server submenu now has a reload action that re-reads ~/.ssh/config and rebuilds the host list live (removing old items, re-checking the configured target). (Uploads already re-read the config per send; only the picker list was stale.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| app | ||
| examples | ||
| extension | ||
| scripts | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
share-receiver
A tiny macOS menu-bar app that receives shared screenshots and uploads them to an SSH server of your choice. After upload it copies the remote path to your clipboard, so you can paste it straight into a remote Claude Code session.
[ Screenshot ] --Share menu--> [ Share Extension ] --pasteboard + Darwin notif--> [ tray app ] --ssh--> [ server ]
|
+--> remote path → clipboard
- Lives in the menu bar (no Dock icon, no window).
- Share menu integration via a native macOS Share Extension.
- Pick the server from the tray — the "Server" submenu lists the host
aliases in your
~/.ssh/config. - Pure-Rust SSH (via
russh) — nosshsubprocess. ResolvesHostName/User/Port/IdentityAgent/IdentityFilefrom~/.ssh/config(includingIncludes) and authenticates through your SSH agent — including a non-defaultIdentityAgentsocket like 1Password. - Uploads to the remote
~/Downloads, falling back to${XDG_DATA_HOME:-~/.local/share}/share-receiver(created if needed).
How the pieces talk
A macOS Share Extension always runs as its own sandboxed process, so it needs
some IPC to hand the image to the tray app. We use a dedicated named
NSPasteboard + a Darwin notification:
- The extension writes the PNG (and original filename) onto a private named pasteboard — never the general clipboard — and posts a Darwin notification.
- The tray app observes that notification (via
notify(3), no run loop) and reads the pasteboard on the main thread. - It uploads over SSH on a background thread, copies the remote path, and shows a notification.
No localhost server, no open port, and the extension needs only the
app-sandbox entitlement (no network). Both processes reach the same named
pasteboard through the system pasteboard server, which works with a free
signing cert (no App Group required).
Build
# Rust core only (for development / running tests):
cargo build
cargo test -- --test-threads=1
# Smoke-test the SSH upload against a real host (uses your ~/.ssh/config + agent):
cargo run --example ssh_smoke -- <ssh-config-alias>
# Full signed app bundle:
./scripts/build_app.sh "Apple Development: you@example.com (TEAMID)"
The bundle is written to dist/ShareReceiver.app.
Signing (required for the Share menu)
macOS will run the menu-bar app when ad-hoc signed, but it generally refuses to load a sandboxed Share Extension unless it's signed with a real identity. A free Apple ID is enough:
- Open Xcode → Settings → Accounts, add your Apple ID (installs an "Apple Development" certificate).
- If
security find-identity -v -p codesigningshows 0 valid identities even though the cert exists, you're likely missing the Apple WWDR G3 intermediate. Install it once:curl -O https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer security import AppleWWDRCAG3.cer -k ~/Library/Keychains/login.keychain-db - Build with that identity:
./scripts/build_app.sh "Apple Development: you@example.com (TEAMID)"
Install & enable
mv dist/ShareReceiver.app /Applications/
open /Applications/ShareReceiver.app
- Enable the extension: System Settings → General → Login Items & Extensions → Sharing → turn on “Share to Receiver”.
- Pick your server: menu-bar icon → Server ▸ → choose a
~/.ssh/confighost. (Or use Edit Settings… to setssh_hostdirectly.) - Test it: menu-bar icon → Test Connection.
Use
Take a screenshot, click its thumbnail (or use the Share button anywhere an image can be shared), and pick Share to Receiver. You'll get a notification and the remote path will be on your clipboard, e.g.:
/home/you/Downloads/1733140800-Screenshot_2026-06-02_at_11.45.00.png
Paste that into your remote Claude Code session.
Configuration
~/.config/share-receiver/config.toml:
# A host alias from ~/.ssh/config. We resolve HostName/User/Port/IdentityAgent
# /IdentityFile from it and connect with a built-in SSH client.
ssh_host = "myserver"
Picking a host from the tray's Server submenu writes this for you. Config is re-read on every upload, so edits take effect without a restart.
Layout
| Path | What |
|---|---|
src/main.rs |
Tray UI, host picker, wiring |
src/ipc.rs |
Named-pasteboard read/write + Darwin-notification observer |
src/upload.rs |
Pure-Rust SSH upload (russh) + clipboard + notify |
src/sshconfig.rs |
~/.ssh/config reader (Include, Host *, IdentityAgent) |
src/config.rs |
TOML config |
src/shared.rs |
Constants shared with the extension |
extension/ |
Swift Share Extension + Info.plist + entitlements |
app/Info.plist |
Containing-app metadata (LSUIElement) |
scripts/build_app.sh |
Assemble + sign the .app |
examples/ssh_smoke.rs |
Manual SSH upload smoke test |
Limitations / notes
- The tray app must be running when you share — Darwin notifications aren't queued for a process that isn't observing.
- Same machine only for the share step (the pasteboard is local).
- Host-key verification is trust-on-first-use (records unknown hosts to
~/.ssh/known_hosts, but refuses a changed key). - SSH-agent auth only (keys in your agent, incl. 1Password). No password auth.
- The remote filename is sanitized to
[A-Za-z0-9._-]and timestamp-prefixed. - Launch at login: toggle Open at Login in the tray menu (uses
SMAppService; also visible under System Settings → Login Items). This only works from the installed, signed.app— not viacargo run.