Relay: local-first remote execution over SSH with managed workspace sync
Project description
Relay
Relay is a local-first remote execution tool for any SSH-accessible host. It makes a remote workspace feel local by handling workspace sync, path mapping, staged transfers, and remote command execution for you.
Use it when you want to keep editing locally but run builds, tests, or tools on a remote Linux machine.
Installation
Relay is a Python-based CLI that depends on several external tools. We recommend installing it using pipx or uv tool to keep it isolated from your system Python.
1. Install external prerequisites
Relay does not install these for you. Ensure they are available in your PATH:
- Local machine:
ssh,mutagen, andrsync. - Remote host:
/bin/sh,python3, andrsync.
See the Prerequisites section for details.
2. Install Relay
# Using uv (recommended)
uv tool install relayctl
# Using pipx
pipx install relayctl
3. Verify installation
Run the doctor command to check your environment:
relayctl doctor
If you have leftover seas artifacts (like a .seas.toml config or a .seas/ directory), relayctl will report them as cleanup issues. These legacy surfaces are unsupported and must be removed or renamed before you can use Relay.
Quick Start
- Ensure prerequisites are met.
- Install
relayctlviapipxoruv tool. - Run
relayctl initin your project root and answer the setup prompt. - Run a command:
relayctl -- pwd(the--separator is required).
By default, relayctl, relayctl --shell, and related top-level commands print live progress updates to stderr as they move through setup, sync, upload, execution, and pullback. Add --verbose to see the individual underlying command steps as well.
Configuration (.relay.toml)
Place a .relay.toml file in your project root to configure the remote host and sync behavior. relayctl init can create it interactively, and you can also start from .relay.example.toml.
[remote]
host = "user@example.com"
workspace_base = "~/.relay/workspaces"
shell = "/bin/sh"
profile = "~/.profile"
[project]
root = "."
exclude = [".git/", ".relay/", ".DS_Store", "__pycache__/", ".pytest_cache/"]
[pull]
enabled = true
conflict_dir = ".relay/conflicts"
[project] controls what gets mirrored into the managed remote workspace. Keep machine-local or generated files in exclude so Mutagen does not try to sync them.
The canonical global config path is ~/.config/relay/config.toml. If an older .seas.toml or ~/.config/seas/config.toml is still present, relayctl fails fast and asks you to rename or delete the legacy file before continuing.
If a stale local .seas/ runtime directory is still present, relayctl warns and ignores it. Relay only creates and uses .relay/ runtime state.
Root Detection
relayctl finds your project root by looking for the nearest .git directory. If no .git directory is found, it defaults to the current working directory. You can override this with the --root flag.
Workspace Synchronization
Relay uses a managed Mutagen workspace for continuous, high-performance file synchronization. This is the only supported synchronization model.
Key Characteristics
- Automatic Session Management:
relayctlautomatically creates and manages a labeled Mutagen session for your project on the first run. - Derived Remote Workspace: The remote workspace path is derived automatically from
remote.workspace_baseand a unique workspace identity. You do not need to configure a manual remote directory. - Sync Ownership: Mutagen owns the synchronization of all files within the project root. Relay-native conflict handling and pullback do not apply to in-project files.
- External Path Staging:
relayctlstill handles the staging of absolute paths from outside your project (e.g.,/tmp/data.txtin your command argv) usingrsync. - Drift Handling: If the managed session's configuration (host, paths, ignores) changes,
relayctlwill detect the "drift". In interactive terminals, it will prompt to recreate the session; in non-interactive environments, it will fail with a clear error.
Lifecycle Commands
Use relayctl mutagen to inspect and control the managed session:
relayctl mutagen status: Show the current session status (absent, healthy, or drifted).relayctl mutagen flush: Force a synchronization cycle (useful before running a command if you just saved a file).relayctl mutagen reset: Manually recreate the session (requires interactive confirmation).relayctl mutagen terminate: Permanently remove the managed session for the current project.
testing
Use the repo's existing commands when validating changes:
uv run pytest -q
just test
just smoke-fake
just smoke-remote is available for a real-host smoke test, but it requires a valid local .relay.toml and a reachable SSH target.
Release automation
GitHub Actions now verifies Relay in two stages:
CIrunsuv run pytest -q, builds distributions withuv build, and install-smokes the built wheel in a clean virtualenv by invoking the installedrelayctlentrypoint.Releasere-runs the same verification before any release action.
Use the Release workflow's manual workflow_dispatch path for a non-publishing beta dry run. Actual PyPI publishing is reserved for beta tags matching v*.*.*b*, and the publish job is gated behind the test, build, and install-smoke jobs with trusted publishing enabled through GitHub's pypi environment.
Before the first real beta publish, register .github/workflows/release.yml as a trusted publisher for the relayctl project on PyPI and protect the repository's pypi environment so release approval stays gated.
Usage
Root command model
Use bare relayctl -- ... for argv-safe execution, and explicit top-level subcommands when you want a different flow:
relayctl -- CMD [ARG ...]: primary bare argv mode with automatic path rewriting.relayctl --shell -- 'SHELL TEXT': explicit shell-text mode (shorthand-s).relayctl ssh: open an interactive shell in the managed workspace.relayctl init: write project config.relayctl mutagen: inspect and control the managed Mutagen session.relayctl doctor: check local prerequisites and stale legacy artifacts, plus remote prerequisites when a concrete host is configured.
If you type an ambiguous root command like relayctl echo ok, Relay will ask you to choose between relayctl -- echo ok and relayctl --shell -- 'echo ok'.
relayctl doctor
Use relayctl doctor to run local diagnostics before your first real remote run or when troubleshooting an environment. When a concrete host is configured, doctor also checks the remote prerequisite contract.
relayctl doctor
relayctl doctor --json
Doctor always checks local ssh, Mutagen usability, and conditional rsync support. It also reports stale local .seas/ directories and matching active legacy Seas-managed Mutagen sessions as cleanup issues. If your config still uses the placeholder host, doctor stays usable for local-only diagnostics and skips remote checks. If a concrete host is configured, doctor also verifies remote /bin/sh, python3, and staged-transfer rsync support. It never tries to install OS packages for you.
relayctl (Bare Argv Mode)
Use bare relayctl -- ... for standard commands where you want automatic path rewriting (argv).
relayctl requires an explicit -- separator before command argv. If omitted, argparse exits with a plain usage error.
Relative argv paths are resolved against your local cwd first. If the resolved path stays inside the project root, Relay rewrites it into the mirrored remote workspace; if it resolves outside the root, Relay stages it as a local external path. Use remote: explicitly when you want a relative token to stay relative to the remote cwd instead.
relayctl -- make test
relayctl -- python3 -m pytest -q
relayctl --verbose -- make build
Deterministic Path Rules:
| Input Pattern | Interpretation | Remote Result |
|---|---|---|
relative/path |
Resolve against local CWD first | Mirror rewrite if in root; otherwise staged as local external |
../outside/path |
Relative path resolving outside project root | Staged to remote slot and rewritten |
/abs/path/in/root |
Inside project root | Mapped to remote mirror path |
/abs/path/outside |
Outside project root | Staged to remote slot and rewritten |
local:relative/or/absolute |
Force local interpretation | Resolve locally, then apply mirror/staging rules |
remote:relative/or/absolute |
Force literal remote path | Passed through unchanged |
- Symlinks that escape the project root are rejected for safety.
- Attached forms (e.g.,
--flag=/tmp/x) are not rewritten in this version.
Note on Redirection: Shell features like
< input.txtor| grep ...are handled by your local shell beforerelayctlruns. To use these features on the remote host, userelayctl --shell.
relayctl --shell (Shell Mode)
Use relayctl --shell (or -s) when you need complex shell features like pipes, redirects, or multiple commands in a single string (shell-mode).
relayctl --shell requires an explicit -- separator before shell text. If omitted, argparse exits with a plain usage error.
relayctl --shell -- 'g++ -o main main.cpp && ./main < input.txt'
relayctl --shell --verbose -- 'g++ -o main main.cpp && ./main < input.txt'
Guardrails and limitations:
- No shell-text parsing:
relayctl --shelldoes not look inside your shell string to rewrite paths (no shell-text parsing). - Explicit staging: If you need files from outside your project, use the
--stageflag (explicit --stage). - Environment:
relayctl --shellexportsRELAY_STAGE_DIR,RELAY_REMOTE_ROOT,RELAY_REMOTE_CWD, andRELAY_RUN_IDto the remote environment. - Legacy runtime state: stale local
.seas/directories are ignored with a warning and are never migrated into.relay/. - limitations:
relayctl --shelldoes not support automatic path rewriting; all paths must be relative to the project root or explicitly staged.
relayctl ssh (Interactive Mode)
Use relayctl ssh to open an interactive shell in your remote workspace.
relayctl ssh
relayctl ssh --workdir remote:/tmp
Guardrails:
- Interactive-only:
relayctl sshdoes not support a command tail or shell-text payload.
Shared Flags
--workdir PATH: Set the remote working directory. Supportsremote:/abs/pathfor literal remote paths. Available for barerelayctl,relayctl --shell, andrelayctl ssh.--env KEY=VAL: Set a remote environment variable. Can be repeated. Keys starting withRELAY_are reserved. Available for barerelayctlandrelayctl --shell.
Conflict Handling (Staged External Paths)
For absolute paths outside your project root (staged external paths), relayctl automatically pulls changed files back to your local machine after the command finishes.
If a local file was modified while the remote command was running, relayctl will not overwrite it. Instead:
- The remote version is saved in
.relay/conflicts/<run-id>/. - A conflict summary is printed.
relayctlexits with code92.
Files within the project root are managed by Mutagen, which handles synchronization and conflict resolution continuously.
exit code
0-255: Remote command exit code is returned unchanged unless a wrapper-reserved condition below applies.90: Local setup/config/runtime/report error.91: Workspace lock timeout (prevents concurrent mirror corruption).92: Pull conflict detected after a successful remote command.
feedback
- Default live feedback: concise phase updates are printed to stderr so you can see progress before the remote command starts producing output.
- Verbose mode: pass
--verboseto print the underlying command steps in addition to the standard phase updates. - Remote command output: stdout and stderr from the remote command still stream normally; progress text stays on stderr.
troubleshooting
- Locking: If a previous run crashed, you might need to manually remove the
.lockfile in the remote workspace directory. - Excludes: Check your
[project].excludelist if files aren't appearing on the remote. - SSH/Rsync: Ensure you have SSH keys set up for passwordless login to the remote host. If you use staged external paths or
--stage, make surersyncexists on both the local and remote machines.
workflow
Example workflows:
# Run a normal command with argv-safe path handling
relayctl -- make test
# Run a shell pipeline remotely
relayctl --shell -- 'make build && ./bin/app < input.txt | tee output.txt'
# Stage an external local file for one remote run
relayctl --shell --stage /tmp/data.csv -- 'python3 scripts/process.py "$RELAY_STAGE_DIR/1/data.csv"'
# Interactive session
relayctl ssh
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file relayctl-0.1.0.tar.gz.
File metadata
- Download URL: relayctl-0.1.0.tar.gz
- Upload date:
- Size: 90.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f03caa02b66826028f502754503e61e731d0c5e4f399de206cce5091648de99b
|
|
| MD5 |
6012826d883902aa4d84a48b7a062e83
|
|
| BLAKE2b-256 |
a291b1c413aa8fe3d47722c624f307f335a794f296c8202a0c444ed322a478b3
|
Provenance
The following attestation bundles were made for relayctl-0.1.0.tar.gz:
Publisher:
release.yml on SpyicyDev/relayctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relayctl-0.1.0.tar.gz -
Subject digest:
f03caa02b66826028f502754503e61e731d0c5e4f399de206cce5091648de99b - Sigstore transparency entry: 1276129358
- Sigstore integration time:
-
Permalink:
SpyicyDev/relayctl@84766d76a75ff8bece9de3fa0bf0cce2f5320ec9 -
Branch / Tag:
refs/tags/v0.1.0b1 - Owner: https://github.com/SpyicyDev
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@84766d76a75ff8bece9de3fa0bf0cce2f5320ec9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file relayctl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: relayctl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 52.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb9574629b1a6cf1b455d18d0c6642305e1b06131afada1227952ca3e55c47fd
|
|
| MD5 |
1a5c9ac493e2542b7447565c882bde4c
|
|
| BLAKE2b-256 |
e692fcb8c73d12164755a24b7ff5fe8eaa64501704254ae5ba1352cfe1c8d2c3
|
Provenance
The following attestation bundles were made for relayctl-0.1.0-py3-none-any.whl:
Publisher:
release.yml on SpyicyDev/relayctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relayctl-0.1.0-py3-none-any.whl -
Subject digest:
bb9574629b1a6cf1b455d18d0c6642305e1b06131afada1227952ca3e55c47fd - Sigstore transparency entry: 1276129363
- Sigstore integration time:
-
Permalink:
SpyicyDev/relayctl@84766d76a75ff8bece9de3fa0bf0cce2f5320ec9 -
Branch / Tag:
refs/tags/v0.1.0b1 - Owner: https://github.com/SpyicyDev
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@84766d76a75ff8bece9de3fa0bf0cce2f5320ec9 -
Trigger Event:
push
-
Statement type: