Secure remote tmux session connector for Claude Code, Codex, and terminal agents.
Project description
SillyJoint
◉─◉ one agent session · every device
SillyJoint is a small, security-first CLI that joins two computers around a single tmux-backed coding-agent session. Start Claude, Codex, aider, or any other terminal agent on one machine; attach to it from another over SSH on your Tailscale network. No public relay, no cloud, no daemons, no stored passwords.
It helps you:
- start or import a local Claude Code, Codex, shell, or custom-agent tmux session
- prepare that session for remote attach over SSH
- send text into a managed session without attaching
- capture recent session output for automation or debugging
- discover devices on your Tailscale tailnet
- attach to the right tmux session from any device
It intentionally does not include memory, schedules, web UI, databases, public relays, shell executors, or cloud services.
The session backend is modular. Today only tmux is implemented:
sillyjoint session start work --agent claude --backend tmux --cwd "$PWD"
Custom agents work by passing the command explicitly:
sillyjoint session start work --agent open-code --cwd "$PWD" --command "opencode"
sillyjoint session start work --agent aider --cwd "$PWD" --command "aider"
Or set a default command via environment variable:
export SILLYJOINT_OPEN_CODE_COMMAND="opencode"
sillyjoint session start work --agent open-code --cwd "$PWD"
iTerm2, Ghostty, GNU screen, and other terminal adapters can be added later without changing the remote connection flow.
Install
Pick whichever fits your setup. All three install dependency-free Python (stdlib-only).
PyPI (once Trusted Publishing is configured — see
docs/RELEASING.md):
pip install sillyjoint
From source:
git clone https://github.com/KiranChilledOut/sillyjointtunnel.git
cd sillyjointtunnel
python3 install.py
Restart your terminal, then:
sillyjoint setup
sillyjoint walkthrough
sillyjoint doctor
sillyjoint skill install
The installer adds sillyjoint to zsh, bash, and PowerShell profiles. To
skip profile changes:
python3 install.py --no-shell-path
Homebrew
Local development formula:
brew install --HEAD ./Formula/sillyjoint.rb
Install from the public GitHub repo:
brew tap KiranChilledOut/sillyjointtunnel https://github.com/KiranChilledOut/sillyjointtunnel.git
brew install KiranChilledOut/sillyjointtunnel/sillyjoint
Homebrew's short tap form looks for a repository named
homebrew-sillyjointtunnel. This project lives in sillyjointtunnel, so
the explicit GitHub URL is required.
Quickstart
If you have zero networking or terminal experience, start with the guided walkthrough. It opens a separate terminal so passwords, browser login steps, and setup commands stay out of any AI chat window:
sillyjoint walkthrough
The walkthrough auto-detects macOS, Linux, Windows/WSL, your shell, Tailscale, tmux, and SSH. It explains how to install Tailscale, sign in, join both devices to the same tailnet, install tmux, enable Remote Login/SSH on the host, and verify the setup. Tailscale is the only network path SillyJoint supports right now.
Remote Login/SSH only needs to be enabled on the computer that will host sessions:
- macOS: System Settings → General → Sharing → Remote Login → On
- Linux: install/start OpenSSH server, usually
sudo systemctl enable --now ssh || sudo service ssh start - WSL Ubuntu: run
sudo service ssh startinside WSL - Native Windows: use WSL as the host path; native OpenSSH Server is optional and requires Administrator setup
Verify on the host:
ssh $(whoami)@127.0.0.1
On the host machine, the beginner path is:
sillyjoint prepare
That opens the guided prepare flow. If it's launched from Claude Code or another non-interactive agent shell, SillyJoint opens a separate terminal first so the questions stay outside the AI chat. The flow asks for the session name, working folder, and agent (Claude, Codex, custom, or shell). The tmux status bar is automatically branded with the SillyJoint palette so every attached terminal looks like part of the product. If you're already inside tmux it offers to import the current session; otherwise it starts a new managed tmux session.
Equivalent low-level commands:
sillyjoint session start work --agent claude --cwd "$PWD"
sillyjoint prepare --session work
On another device on the same tailnet:
sillyjoint probe
sillyjoint connect
sillyjoint ask "what is the current git status?"
sillyjoint send "run tests"
sillyjoint capture
probe checks SSH access, remote SillyJoint availability, tmux, and
whether the target session is registered and alive before attaching. ask
sends one prompt, waits, and captures recent output in a single SSH call.
send types into the remote session over SSH; capture reads recent
remote output without attaching interactively. connect lists Tailscale
devices, lets you search/select a host, asks for SSH username and session
name, then runs:
ssh -t user@host 'sh -lc "sillyjoint session attach work"'
For SSH commands, the remote shell often doesn't load the same PATH as a
normal interactive login. SillyJoint checks common install paths like
~/.local/bin, ~/bin, /opt/homebrew/bin, and /usr/local/bin. If the
remote host still can't find sillyjoint, pass the full path:
sillyjoint connect --remote-command /Users/kiran/.local/bin/sillyjoint
sillyjoint connect --remote-command /home/chilledout/.local/bin/sillyjoint
--remote-command is the SillyJoint executable on the remote machine. It
is not the agent command. Don't enter claude --proxy there; start
that agent inside a remote tmux session first.
How sessions work
SillyJoint is the way you launch a coding-agent session that you want
to keep using — including from other devices later. Instead of typing
claude or codex, type:
sillyjoint prepare
That starts your agent inside a managed tmux session, registers it, and
drops you straight into it so you can start working immediately. When
you're done for the day, detach with Ctrl-b d and close the terminal —
the session keeps running. From any other device on your Tailscale
network, sillyjoint connect picks up where you left off.
Scripted equivalent:
sillyjoint session start work --agent claude --cwd "$PWD"
sillyjoint session attach work
If you want the session to start without auto-attaching (e.g. you're
preparing the host for someone else to connect to), pass --no-attach.
Already inside tmux? Register the current tmux session instead of starting a new one. The same tmux keeps running:
sillyjoint session import --name work --agent claude --cwd "$PWD"
sillyjoint prepare --session work
Not in tmux? Use sillyjoint session start (above). If you have an
existing conversation in your agent that you'd like to keep, pass the
agent's resume flag as the launch command:
# Claude — continue last conversation
sillyjoint session start work --agent claude --cwd "$PWD" --command "claude --proxy -c"
# Codex — resume last session
sillyjoint session start work --agent codex --cwd "$PWD" --command "codex resume"
-c (Claude) and resume (Codex) preserve conversation state, so
restarting inside SillyJoint is effectively the same as continuing — just
with the bonus that the new session is remote-attachable.
Remote profiles
Save host, SSH user, session name, and remote SillyJoint executable as a named profile so you don't retype them:
sillyjoint remote add linux-work
Non-interactive equivalent:
sillyjoint remote save linux-work \
--host 100.115.18.100 \
--user chilledout \
--session work \
--remote-command /home/chilledout/.local/bin/sillyjoint
Validate before saving:
sillyjoint remote add linux-work --host 100.115.18.100 --user chilledout --probe
Then reuse the profile:
sillyjoint probe --profile linux-work
sillyjoint ask "summarize the current task" --profile linux-work --wait-until-quiet
sillyjoint connect --profile linux-work --probe
Profiles live in ~/.sillyjoint/remotes.json. They do not store
passwords, tokens, prompts, or API keys.
connect --probe runs a remote preflight before attaching. It refuses to
attach if the remote SillyJoint executable is missing, tmux is missing,
or the target session isn't registered/alive.
Speed-up: SSH ControlMaster
By default, every remote command (probe, connect, send, capture, ask) opens its own SSH connection — handshake, auth, run, close. For multi-command workflows that gets slow and prompts for your password repeatedly.
sillyjoint ssh-config enable writes a clearly-marked block into
~/.ssh/config that enables ControlMaster auto for Host sillyjoint-*
and aliases each saved profile as sillyjoint-<name>. Subsequent SSH
calls reuse a single connection per host for 10 minutes:
sillyjoint ssh-config enable
After enabling, every remote add / remote save / remote remove
auto-refreshes the block so the aliases stay in sync. To preview what
will be written:
sillyjoint ssh-config show
To remove the block (leaves the rest of your SSH config untouched):
sillyjoint ssh-config disable
Per-call escape hatch if you want one connection to bypass ControlMaster:
sillyjoint connect --profile macbook --no-controlmaster
This is fully opt-in. sillyjoint setup never touches ~/.ssh/config on
its own.
Status at a glance
One command that shows everything: local sessions, remote profiles,
ControlMaster state (with cm:live badges on profiles whose SSH socket
is currently warm), and environment dependencies:
sillyjoint status
JSON for automation:
sillyjoint status --json
Commands
See COMMANDS.md for the full command reference. Useful daily commands:
sillyjoint status
sillyjoint prepare
sillyjoint connect
sillyjoint ask "what is the current task?" --wait-until-quiet
sillyjoint session list
sillyjoint session prune
sillyjoint ssh-config enable # one-time speed-up
Mac → WSL/Linux example
On the Linux/WSL host:
python3 install.py
~/.local/bin/sillyjoint setup
~/.local/bin/sillyjoint session start work --agent claude --cwd "$PWD" --command "claude --proxy"
From the Mac:
sillyjoint connect \
--host 100.115.18.100 \
--user chilledout \
--session work \
--remote-command /home/chilledout/.local/bin/sillyjoint
SSH prompts for your password or uses your existing keys. SillyJoint does not store SSH passwords.
For longer agent replies, let ask wait until pane output stops
changing:
sillyjoint ask "run the tests and summarize failures" --profile linux-work --wait-until-quiet
The quiet wait is bounded; tune it when needed:
sillyjoint ask "review this branch" --profile linux-work \
--wait-until-quiet --quiet-seconds 3 --max-wait-seconds 120
Security model
SillyJoint is a coordinator, not a relay.
- no public terminal relay
- no password storage
- no raw secret collection
- no command execution API
- no background daemon
- no cloud dependency
- SSH remains the authority boundary
- Tailscale remains the network boundary for now
See SECURITY.md.
Docs
The docs in docs/ are structured so sillyjoint.com can later publish
them as the product documentation site.
Development
make test
make verify
Project details
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 sillyjoint-0.1.37.tar.gz.
File metadata
- Download URL: sillyjoint-0.1.37.tar.gz
- Upload date:
- Size: 48.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4200825f31606207943abe83d677f9510448784348ba0d1d8fb9a1c4b06383f
|
|
| MD5 |
15cd2df1dc63ed8f36489da2f36d76ec
|
|
| BLAKE2b-256 |
8858e9de9cc53ae7b81b6cd154ace85fcc161e51b9b0e1d8900fbb03b9aa2633
|
Provenance
The following attestation bundles were made for sillyjoint-0.1.37.tar.gz:
Publisher:
release.yml on KiranChilledOut/sillyjointtunnel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sillyjoint-0.1.37.tar.gz -
Subject digest:
a4200825f31606207943abe83d677f9510448784348ba0d1d8fb9a1c4b06383f - Sigstore transparency entry: 1511705143
- Sigstore integration time:
-
Permalink:
KiranChilledOut/sillyjointtunnel@3ee34f3e1d58f19e6645e8ffc8ce0247c28a396a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/KiranChilledOut
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3ee34f3e1d58f19e6645e8ffc8ce0247c28a396a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file sillyjoint-0.1.37-py3-none-any.whl.
File metadata
- Download URL: sillyjoint-0.1.37-py3-none-any.whl
- Upload date:
- Size: 51.0 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 |
86d4b0570617f07f31f7f82d4d092c2cb46c33f530de85f3b060f57892670d94
|
|
| MD5 |
c5680a1277b4d203137269d1ac7ff401
|
|
| BLAKE2b-256 |
5dbc64bb4cba5275677baa5fc94967d4fde73deccf8ccbd295c77eb42f3e363e
|
Provenance
The following attestation bundles were made for sillyjoint-0.1.37-py3-none-any.whl:
Publisher:
release.yml on KiranChilledOut/sillyjointtunnel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sillyjoint-0.1.37-py3-none-any.whl -
Subject digest:
86d4b0570617f07f31f7f82d4d092c2cb46c33f530de85f3b060f57892670d94 - Sigstore transparency entry: 1511705285
- Sigstore integration time:
-
Permalink:
KiranChilledOut/sillyjointtunnel@3ee34f3e1d58f19e6645e8ffc8ce0247c28a396a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/KiranChilledOut
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3ee34f3e1d58f19e6645e8ffc8ce0247c28a396a -
Trigger Event:
workflow_dispatch
-
Statement type: