Skip to main content

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:

pip install sillyjoint

Homebrew (see also the ## Homebrew section below):

brew tap KiranChilledOut/sillyjointtunnel https://github.com/KiranChilledOut/sillyjointtunnel.git
brew install KiranChilledOut/sillyjointtunnel/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

Update

Whichever install method you used, the update command is the same:

sillyjoint update

SillyJoint detects from its own executable path how it was installed — Homebrew, pip, or python3 install.py — and runs the right upgrade for that method. Check for an update without installing it with sillyjoint update --check. Force a specific path with sillyjoint update --method brew|pip|source.

sillyjoint setup, doctor, and status quietly check PyPI once per 24h and print a single hint line when a newer release is available. Silent if the network is unavailable.

If sillyjoint --version keeps showing an older number after you upgrade, you probably have two sillyjoint binaries on PATH (a common case is running both python3 install.py and brew install on the same machine). Run sillyjoint doctor — it now detects this, shows you the active binary in mint and any shadowed ones in yellow, and tells you exactly what to remove.

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 start inside 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
sillyjoint update                 # self-update from any install method

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

sillyjoint-0.1.40.tar.gz (54.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sillyjoint-0.1.40-py3-none-any.whl (56.9 kB view details)

Uploaded Python 3

File details

Details for the file sillyjoint-0.1.40.tar.gz.

File metadata

  • Download URL: sillyjoint-0.1.40.tar.gz
  • Upload date:
  • Size: 54.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sillyjoint-0.1.40.tar.gz
Algorithm Hash digest
SHA256 a9291dd980170e5996ceec0cb4d5d7f079c3991f56497ab2c165ae9f64acd37d
MD5 37c8fde145c55aacc00c73ff2687cff2
BLAKE2b-256 e80c5a9354a45560b3988ce44fb52ad02a438bf8ecb15064a1f7bed10de98eb1

See more details on using hashes here.

Provenance

The following attestation bundles were made for sillyjoint-0.1.40.tar.gz:

Publisher: release.yml on KiranChilledOut/sillyjointtunnel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sillyjoint-0.1.40-py3-none-any.whl.

File metadata

  • Download URL: sillyjoint-0.1.40-py3-none-any.whl
  • Upload date:
  • Size: 56.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sillyjoint-0.1.40-py3-none-any.whl
Algorithm Hash digest
SHA256 7fbb81294ee1f044caaccd9b164b1bc5319813fd81d867d817f6129b8d12b271
MD5 e6fd60ee65dd0ca588b3bcff2f8ef822
BLAKE2b-256 258e6d310661d7ee4639fb909e84e0deabae8e812cbf3c4108131056f3691334

See more details on using hashes here.

Provenance

The following attestation bundles were made for sillyjoint-0.1.40-py3-none-any.whl:

Publisher: release.yml on KiranChilledOut/sillyjointtunnel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page