Agent client (shellwatch-agent)
shellwatch-agent is the Go thin client for the SSH agent proxy. It listens on a local socket and relays SSH agent protocol frames over WebSocket to ShellWatch.
Releases: github.com/rado0x54/ShellWatch/releases?q=agent. Source: agent-client/. Homebrew tap: rado0x54/homebrew-tap.
Subcommands
Section titled “Subcommands”shellwatch-agent # daemon (default)shellwatch-agent login # browser-based enrolment via OAuth DCR + PKCEshellwatch-agent logout # remove stored credentials from the credstoreshellwatch-agent --print-env # print the SSH_AUTH_SOCK export line| Flag | Env var | Applies to | Description |
|---|---|---|---|
--server | SHELLWATCH_SERVER | daemon, login, logout | ShellWatch server URL. Default: https://app.shellwatch.ai. Login keys credentials by URL — pass the same value to both login and the daemon. |
--token | SHELLWATCH_TOKEN | daemon | Static agent-scoped bearer token. Skips the credstore. Short-lived, not refreshed. |
--socket | SHELLWATCH_AGENT_SOCK | daemon, --print-env | Listener path. Default: platform-appropriate (see below). |
--insecure | — | daemon, login | Daemon: allow ws://. Login: allow http://. Local dev only. |
--print-env | — | (mode itself) | Print export SSH_AUTH_SOCK=<path> (or PowerShell-friendly form on Windows) and exit. |
--token and --socket are silently ignored by login / logout. Precedence: CLI flags > environment variables > credstore > defaults.
Default listener path
Section titled “Default listener path”| Platform | Path |
|---|---|
| Linux | $XDG_RUNTIME_DIR/shellwatch-agent.sock if set, else ${TMPDIR:-/tmp}/shellwatch-agent-<uid>.sock |
| macOS | ${TMPDIR}/shellwatch-agent-<uid>.sock (launchd-set per-user /var/folders/.../T/, not /tmp) |
| Windows | \\.\pipe\openssh-ssh-agent (matches stock OpenSSH-for-Windows) |
The path is stable across restarts, so eval "$(shellwatch-agent --print-env)" from a shell profile works fine.
login — OAuth DCR + PKCE flow
Section titled “login — OAuth DCR + PKCE flow”shellwatch-agent login runs a full OAuth 2.1 authorization_code + PKCE flow against the server’s OAuth authority (Ory Hydra), discovered via RFC 8414:
- Fetch
${server}/.well-known/oauth-authorization-serverto discover the authorization, token, and registration endpoints. - Register a public client at
${server}/api/hydra/register(mediated DCR — the loopback redirect URI is validated against server policy, which allows loopback by default). - Generate a PKCE verifier and S256 challenge; bind a one-shot HTTP listener on
127.0.0.1:0. - Open the user’s browser to the authorization endpoint (
scope=agent offline_access). - The user logs in with their passkey and consents.
- Hydra redirects to the loopback URL with a code; the agent verifies state and exchanges it at the token endpoint.
- The resulting refresh token is persisted via the credstore. The daemon mints short-lived (~30 min)
agent-scoped access tokens from it at startup and renews them silently.
logout only removes the local copy; it does not revoke the grant server-side. To revoke remotely, sign out / revoke the device’s sessions in ShellWatch.
Credential store
Section titled “Credential store”| Platform | Primary | Fallback (mode 0600) |
|---|---|---|
| macOS | Keychain (via security) | ~/Library/Application Support/shellwatch/credentials |
| Linux | libsecret D-Bus (gnome-keyring / KWallet) | ${XDG_CONFIG_HOME:-~/.config}/shellwatch/credentials |
| Windows | DPAPI / Credential Manager | %AppData%\shellwatch\credentials (parent-ACL protected) |
Fallback is used when no keyring is reachable in the current session. Credentials are keyed by server URL — you can hold credentials for multiple instances simultaneously.
The fallback file is JSON, keyed by server URL.
Windows fallback file note: Go’s
os.Chmodonly flips the read-only bit on Windows, not NTFS ACLs. Protection comes from%AppData%’s parent-directory ACLs, which Windows configures user-only by default. Prefer the keyring on multi-user boxes.
Handshake headers
Section titled “Handshake headers”On WebSocket connect to /agent-proxy, the client sends:
| Header | Value |
|---|---|
Authorization | Bearer <access-token> |
X-ShellWatch-Hostname | os.Hostname() |
X-ShellWatch-OS | runtime.GOOS (linux, darwin, windows, …) |
X-ShellWatch-Version | Version (set at link time via -ldflags "-X main.Version=...") |
The server sanitises these and exposes them to the approver on /sign/:id in a separate “self-reported” block. They are a hint, not authoritative identity. See Concepts → Sign requests.
Requires Go 1.21+.
cd agent-clientmake build # version derived from `git describe`make build VERSION=0.1.0 # pin a specific versionCross-compile:
GOOS=linux GOARCH=amd64 go build -o shellwatch-agent-linux-amd64 ./cmd/shellwatch-agent/GOOS=linux GOARCH=arm64 go build -o shellwatch-agent-linux-arm64 ./cmd/shellwatch-agent/GOOS=darwin GOARCH=amd64 go build -o shellwatch-agent-darwin-amd64 ./cmd/shellwatch-agent/GOOS=darwin GOARCH=arm64 go build -o shellwatch-agent-darwin-arm64 ./cmd/shellwatch-agent/GOOS=windows GOARCH=amd64 go build -o shellwatch-agent-windows-amd64.exe ./cmd/shellwatch-agent/Daemon behaviour
Section titled “Daemon behaviour”- Each Unix-socket / named-pipe connection from a local SSH client gets its own outbound WebSocket. Concurrent SSH clients don’t share state.
- WebSocket-level keepalive detects dead connections within ~1 minute; the dialer reconnects with exponential backoff on the next SSH operation. Laptop sleep, network changes, and brief server outages are handled transparently.
SSH_AGENTC_EXTENSION(type 27) frames from newer OpenSSH (session binding) are answered locally withSSH_AGENT_FAILURErather than forwarded — this works around anssh2AgentProtocolparse bug for unknown extensions with payloads. Invisible in normal use.- The process exits on first unrecoverable error (e.g. socket already in use, server returns 401). Run it under a process supervisor for auto-restart — the Homebrew formula’s
service doblock, a launchd plist, or asystemd --userunit all work.
Known limitations
Section titled “Known limitations”- OpenSSH 10.3+ on the local client for passkey signing. Earlier versions reject the
webauthn-...signature type. See SSH agent proxy → Requires OpenSSH 10.3+. - A browser session must be open in ShellWatch for passkey signs to be approvable in real time. Web Push helps when the tab is closed.
- Windows service installation isn’t yet wired into the CLI — wrap with
nssm/sc.exeif you want auto-start at logon. Roadmap item. - Custom
--serverfor the brew service. Theservice doblock uses the defaultapp.shellwatch.ai; self-hosted instances need the manual launchd / systemd-user setup. Roadmap item.
Troubleshooting
Section titled “Troubleshooting”See Guides → SSH agent proxy → Troubleshooting and the agent-client README for the canonical list (signature-type errors, missing browser session, sign timeouts, missing credentials after a server-URL mismatch).