Metadata-Version: 2.4
Name: sillyjoint
Version: 0.1.41
Summary: Secure remote tmux session connector for Claude Code, Codex, and terminal agents.
Author: Kiran Raj Rajendran
License: MIT
Project-URL: Homepage, https://sillyjoint.com
Project-URL: Repository, https://github.com/KiranChilledOut/sillyjointtunnel
Project-URL: Issues, https://github.com/KiranChilledOut/sillyjointtunnel/issues
Keywords: tmux,ssh,tailscale,remote-session,claude-code,codex
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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:

```bash
sillyjoint session start work --agent claude --backend tmux --cwd "$PWD"
```

Custom agents work by passing the command explicitly:

```bash
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:

```bash
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**:

```bash
pip install sillyjoint
```

**Homebrew** (see also the [`## Homebrew`](#homebrew) section below):

```bash
brew tap KiranChilledOut/sillyjointtunnel https://github.com/KiranChilledOut/sillyjointtunnel.git
brew install KiranChilledOut/sillyjointtunnel/sillyjoint
```

**From source**:

```bash
git clone https://github.com/KiranChilledOut/sillyjointtunnel.git
cd sillyjointtunnel
python3 install.py
```

Restart your terminal, then:

```bash
sillyjoint setup
sillyjoint walkthrough
sillyjoint doctor
sillyjoint skill install
```

The installer adds `sillyjoint` to zsh, bash, and PowerShell profiles. To
skip profile changes:

```bash
python3 install.py --no-shell-path
```

## Update

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

```bash
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:

```bash
brew install --HEAD ./Formula/sillyjoint.rb
```

Install from the public GitHub repo:

```bash
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:

```bash
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:

```bash
ssh $(whoami)@127.0.0.1
```

On the host machine, the beginner path is:

```bash
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:

```bash
sillyjoint session start work --agent claude --cwd "$PWD"
sillyjoint prepare --session work
```

On another device on the same tailnet:

```bash
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:

```text
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
# 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:

```bash
sillyjoint remote add linux-work
```

Non-interactive equivalent:

```bash
sillyjoint remote save linux-work \
  --host 100.115.18.100 \
  --user chilledout \
  --session work \
  --remote-command /home/chilledout/.local/bin/sillyjoint
```

Validate before saving:

```bash
sillyjoint remote add linux-work --host 100.115.18.100 --user chilledout --probe
```

Then reuse the profile:

```bash
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:

```bash
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:

```bash
sillyjoint ssh-config show
```

To remove the block (leaves the rest of your SSH config untouched):

```bash
sillyjoint ssh-config disable
```

Per-call escape hatch if you want one connection to bypass ControlMaster:

```bash
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:

```bash
sillyjoint status
```

JSON for automation:

```bash
sillyjoint status --json
```

## Commands

See [COMMANDS.md](COMMANDS.md) for the full command reference. Useful
daily commands:

```bash
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:

```bash
python3 install.py
~/.local/bin/sillyjoint setup
~/.local/bin/sillyjoint session start work --agent claude --cwd "$PWD" --command "claude --proxy"
```

From the Mac:

```bash
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:

```bash
sillyjoint ask "run the tests and summarize failures" --profile linux-work --wait-until-quiet
```

The quiet wait is bounded; tune it when needed:

```bash
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](SECURITY.md).

## Docs

The docs in `docs/` are structured so `sillyjoint.com` can later publish
them as the product documentation site.

## Development

```bash
make test
make verify
```
