# tmux-party

![l'esprit de la fête](tmux-party.gif)

Share a live tmux session with your buddies on a wide range of UNICES!

`tmux-party` ships `party`, a POSIX shell utility. One person hosts a party, and others join the fun! Wanna pair-program, mentor, debug an outage together, or demo a workflow? `ssh` in, `party join` & off you go!

Built for small, mutually trusted groups: a hacklab, a tech team, a circle of friends, not strangers across the internet. Inspired by epic sessions running GNU's `screen -x` way back when, now on BSD's `tmux` modern codebase!

## Requirements

- POSIX shell (`/bin/sh`)
- `tmux` ≥ 3.3 (for `server-access`) on `$PATH`, or named via `PARTY_TMUX`
- A shared system group. A party is gated on the first of `staff`, `users`, `wheel`, `party` the host belongs to (`--group <name>` or `TMUX_PARTY_GROUP` always win). Boxes where people already share one of those groups need no setup; creating a dedicated `party` group is the fallback recipe. Every host and guest must be a member of whichever group ends up gating the party.
- `write(1)`, optional: used to ping invited guests. Silently skipped when absent.

## Install

```sh
make install                       # installs to /usr/local
make install PREFIX=$HOME/.local   # or any prefix you prefer
```

Installs `party` to `$(PREFIX)/bin` and `party.1` to `$(PREFIX)/share/man/man1`; `man party` is the full reference. `make uninstall` removes both.

On illumos/Solaris the default prefix is `/opt/party` and the install needs root (`pfexec make install`) for the system bindir and `/etc/default/login`. The Makefile is plain POSIX make (works with `/usr/bin/make`, BSD make, GNU make).

## Quick start

One-time setup, as root. Create the group and add members:

```sh
groupadd party
usermod -aG party alice
usermod -aG party bob
# alice and bob log out and back in so the new group takes effect
```

Host a party, then invite from inside it:

```sh
party host standup        # spawn a dedicated party tmux server
party invite alice        # add alice to the allowlist (read/write)
party invite bob -r       # invite bob as a read-only watcher
```

Guests join:

```sh
party list                # see what's running on this host
party join standup        # attach (auto-attaches if only one party is running)
party leave               # detach and clean up
```

The `party` group is the partyline: filesystem access to party sockets is shared among members, and `tmux server-access` decides who is actually let into a given session. No shared install directory is needed: each party keeps its own state under its own per-party private directory. Parties you haven't been invited to yet show up in `party list` as `invite-only`; `party join` on one tells you which host to ask (exit status 13).

## How it works

`party host` spawns a *dedicated* tmux server for the party (separate from your personal tmux) so inviting someone exposes only the party, not your other sessions. Each party gets a private directory at `${PARTY_SOCKET_DIR}/party-${USER}:${PARTY_NAME}.d/`, holding the tmux socket and a join/leave notification helper. Guests never type the path; `party join` reads the roster and handles the attach.

Two gates protect every party, and both must pass:

- **Filesystem gate**: the per-party directory is `chgrp`'d to `TMUX_PARTY_GROUP` at mode `0750`; the socket is `g+rw`. Non-members can't traverse in or reach the socket.
- **Auth gate**: `tmux server-access` allowlist, editable only by the host. A user who clears the FS gate but isn't on the allowlist is rejected at the protocol layer.

Party metadata (host, server pid, group, creation time) lives in a `roster` file beside the socket. It's bookkeeping only, as security-relevant fields (host user, socket path) are re-derived from the directory on every read, never trusted from the file.

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `TMUX_PARTY_GROUP` | auto-selected | Shared group for socket access. When unset, `party host` auto-selects (see `PARTY_GROUP_CANDIDATES`); pin an existing group here, or pass `--group <name>` to `party host`. |
| `PARTY_GROUP_CANDIDATES` | `staff users wheel party` | Space-separated candidate list `party host` walks when `TMUX_PARTY_GROUP` is unset; gates on the first the host belongs to. |
| `PARTY_SOCKET_DIR` | `/tmp` | Where each party's private directory (socket + roster) is created. No whitespace in the path. |
| `PARTY_TMUX` | `tmux` | tmux binary to use. Override if tmux ≥ 3.3 lives at a non-standard path. |

## Support

The mechanism is identical everywhere: group ownership and mode bits plus `tmux server-access`. No ACL syscalls involved, so the trust path is one piece of code, not a per-OS matrix.

| Platform | Status |
|---|---|
| Linux | supported, validated on Debian 13 & Alpine v3.21 |
| FreeBSD UFS or ZFS | supported, validated on FreeBSD 15.0 ZFS |
| illumos LX-branded zones | supported, validated on 4.4 BrandZ linux |
| illumos native, Solaris | supported, validated on omnios-r151058 |
| NetBSD, DragonFly | unconfirmed, should work, tests welcome |
| OpenBSD | supported, validated on OpenBSD 7.8 |
| macOS | supported, validated on Darwin 25.5.0 |

## Security

`party` assumes you already know and trust everyone you add to the group; it is not a public access-control system. Both gates above protect each party, and the **auth gate** (`tmux server-access`) is authoritative, with the filesystem gate as defense in depth.

The threat model is **accidents, not adversaries**. `party` runs where everyone already shares the box, so the ownership and symlink gates in front of destructive operations (`close`'s directory removal, moderation) are there so a stale roster, a typo, or two racing `party host` invocations can't remove or hijack the wrong thing: that the same checks would also stop a malicious group member is a side effect, not the design goal. New checks should earn their place by preventing a plausible *mistake*, not by closing a hypothetical attack from a friend.

Three honest caveats, with the full detail in `man party`:

- On ACL-enabled filesystems (ZFS, HFS+/APFS), inherited ACLs can override the mode bits, so the FS gate is best-effort. The auth gate still holds.
- A party's *existence* is not hidden the way *attaching* is. That confidentiality rides on the FS gate.
- Active guests share one tmux server, where any write-capable invitee is trusted by design. Invite read-only (`-r`) if you do not trust that far.
- `party log`'s transcript is group-readable, not invitee-readable: any member of the party's group can read it, including group members who were never invited.

## Status

`party` is pre-release. The on-disk layout (per-party directory, roster format, socket path) may change between commits, with no upgrade migration, so close running parties before pulling. A stale roster pointing at an unreadable socket simply won't be discovered; old tmux servers left running must be killed by hand.

## Reference

| Subcommand | Effect |
|---|---|
| `party host [name]` | Spawn a dedicated party tmux server. Only the host is on the allowlist. Names: letters, digits, `_`, `-`; max 63 chars. |
| `party invite alice [-r]` | Add alice to the allowlist. `-r` / `--read-only` invites as a watcher. |
| `party voice alice` | Promote alice to read/write. `--party <name>` selects among several hosted parties. |
| `party mute alice` | Demote alice to read-only. `--party <name>` selects among several hosted parties. |
| `party kick alice` | Revoke alice's invite, disconnect her, kill her guest session. `--party <name>` selects among several hosted parties. |
| `party detach alice` | Disconnect alice; keep her on the allowlist. `--party <name>` selects among several hosted parties. |
| `party list` | List live parties on this host. |
| `party who [--short]` | Show invited and attached users for the current party. |
| `party status` | Show the caller's own state: hosting, attached, or idle. |
| `party close` | Tear down the party server and its roster entry. Host-only. |
| `party clean` | Remove the caller's own dead party dirs: crash leftovers whose tmux server is gone. |
| `party log [--stop]` | Record the party into its private dir via `tmux pipe-pane` (host only), group-readable. `--stop` ends it. |
| `party join [name] [--passive]` | Join a party. Auto-attaches when one is running; picker otherwise. `--passive` attaches read-only to the host's view (watcher mode). Read-only invitees always join passive. |
| `party leave` | Detach and clean up the per-guest session. |
| `party knock <name>` | Ask the host of an invite-only party for an invite (write(1) ping with the exact invite command). |
| `party role [active\|passive\|switch]` | Flip your clients between guest and host session. No arg prints the current role. |
| `party --help` | Help text. |

## Credits

Written by veg and kol3rby in the context of the [UNIX Social Club](https://club.unix.rocks/). Issues and contributions welcome.
