tmux-party/party.1
veg 6be0ac1877 Initial pre-release
tmux-party: share a tmux session with people on the same UNIX host.
Single-file POSIX shell (party) with a filesystem + tmux server-access
trust model. See README.md.
2026-06-01 15:31:54 +00:00

435 lines
9.8 KiB
Groff

.\" party - share a tmux session with the people you already work with
.Dd April 30, 2026
.Dt PARTY 1
.Os
.Sh NAME
.Nm party
.Nd share a tmux session with the people you already work with
.Sh SYNOPSIS
.Nm
.Cm host
.Op Ar name
.Op Fl -group Ar group
.Nm
.Cm close
.Op Ar name
.Nm
.Cm join
.Op Ar name
.Op Fl -passive
.Nm
.Cm leave
.Nm
.Cm invite
.Ar user
.Op Fl r
.Nm
.Cm voice
.Ar user
.Nm
.Cm mute
.Ar user
.Nm
.Cm kick
.Ar user
.Nm
.Cm detach
.Ar user
.Nm
.Cm role
.Op Cm active | passive | switch
.Nm
.Cm list
.Nm
.Cm who
.Op Fl -short
.Nm
.Cm status
.Sh DESCRIPTION
.Nm
hosts a live
.Xr tmux 1
session for other users on the same UNIX host, or joins one
someone else is hosting, with the ergonomics of
.Ql screen -x
and none of the copy/paste friction.
Use it to pair-program, mentor, debug an outage together, or demo a
workflow live: same terminal, no screen-sharing software, no
intermediary servers.
.Pp
Each party runs on its own dedicated
.Xr tmux 1
server, distinct from the host's personal
.Xr tmux 1 .
This keeps
.Xr tmux 1
.Cm server-access
scoped: inviting someone to your party does not expose your other
sessions.
Two gates protect each party.
The
.Em filesystem gate
is universal across every supported platform: the per-party directory
is
.Xr chgrp 1 Ns 'd
to
.Ev TMUX_PARTY_GROUP
at mode 0750 so only group members can traverse into it; the socket
itself is
.Ql g+rw .
The
.Em auth gate
is
.Xr tmux 1
.Cm server-access :
a user who passes the FS gate but is not on the allowlist is rejected
at the protocol layer.
Both gates must pass.
.Pp
.Nm
is designed for small, mutually trusted groups: a hacklab, a tech
team, a circle of friends, not strangers across the internet.
The auth gate is authoritative; the FS gate is defense in depth.
.Sh SUBCOMMANDS
.Bl -tag -width Ds
.It Cm host Op Ar name Op Fl -group Ar group
Spawn a dedicated party
.Xr tmux 1
server.
With no
.Ar name ,
a random one is generated.
Only the host is on the allowlist; invite others explicitly with
.Cm invite .
Group precedence:
.Fl -group
flag >
.Ev TMUX_PARTY_GROUP
> the default
.Ql party .
.It Cm close Op Ar name
Tear down the party server and remove its per-party directory.
With no
.Ar name ,
closes the only party the caller hosts.
Host-only.
.It Cm join Op Ar name Op Fl -passive
Join a party.
With no
.Ar name
and exactly one party visible, auto-attaches; otherwise presents a
numbered picker.
By default joins in
.Em active
mode (your own session group with an independent window cursor).
.Fl -passive
attaches read-only to the host's session (mirrored view).
The read-only boundary is set at attach time and is not toggled by
later
.Cm role
changes.
.It Cm leave
Detach from the current party and clean up the per-guest session.
.It Cm invite Ar user Op Fl r
Add
.Ar user
to the allowlist as read/write.
.Fl r
or
.Fl -read-only
invites as a watcher.
Best-effort
.Xr write 1
ping is sent if available.
Host-only.
.It Cm voice Ar user
Promote
.Ar user
to read/write.
Alias:
.Cm rw .
Host-only.
.It Cm mute Ar user
Demote
.Ar user
to read-only.
Alias:
.Cm ro .
Host-only.
.It Cm kick Ar user
Revoke
.Ar user Ns 's
invite, disconnect their client, and kill their guest session.
Host-only.
.It Cm detach Ar user
Disconnect
.Ar user Ns 's
client and kill their guest session, but keep them on the allowlist.
Host-only.
.It Cm role Op Cm active | passive | switch
Flip your clients between the guest session and the host session.
With no argument, prints the current role for each of your clients.
Session-level only: the read-only boundary set at attach time
.Po see
.Cm join Fl -passive
.Pc
is not toggled by role changes
.Po
.Xr tmux 1
.Cm switch-client
has no read-only flag
.Pc .
.It Cm list
List live parties on this host from the roster.
.It Cm who Op Fl -short
Show invited and attached users for the current party.
.Fl -short
emits a compact form suitable for a status-line widget.
.It Cm status
Show the caller's own state: hosting, attached, or idle.
.El
.Sh ENVIRONMENT
.Bl -tag -width "PARTY_SOCKET_DIR"
.It Ev TMUX_PARTY_GROUP
Shared system group used for socket access.
Default
.Ql party .
Override to reuse an existing group like
.Ql wheel ,
.Ql users ,
or
.Ql staff ,
or pass
.Fl -group Ar name
to
.Cm host .
.It Ev PARTY_SOCKET_DIR
Where each party's per-party private directory (and its socket and
roster) is created.
Default
.Pa /tmp .
.It Ev PARTY_TMUX
.Xr tmux 1
binary to use.
Default
.Ql tmux .
Override if
.Xr tmux 1
\(>= 3.3 lives at a non-standard path.
.El
.Sh FILES
.Bl -tag -width Ds
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/
Per-party private directory, mode 0750, group
.Ev TMUX_PARTY_GROUP .
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/sock
The
.Xr tmux 1
socket, mode
.Ql g+rw .
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/roster
Bookkeeping:
.Va HOST_USER ,
.Va PARTY_NAME ,
.Va SOCKET ,
.Va SERVER_PID ,
.Va GROUP ,
.Va CREATED .
Security-relevant fields are re-derived from the directory itself on
every read, not trusted from the file.
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/.party-notify
Auto-generated helper that fans
.Cm display-message
out to every attached client on join/leave.
.El
.Sh EXIT STATUS
.Bl -tag -width Ds
.It 0
Success.
.It 1
Operational failure (no such party, not a member of the group, socket
gone, ambiguous match, etc.).
.It 2
Usage error (unknown subcommand or flag, missing argument, invalid
party name).
.It 13
.Cm join :
caller is not on the allowlist for the named party.
.El
.Sh EXAMPLES
One-time setup, as root:
.Bd -literal -offset indent
groupadd party
usermod -aG party alice
usermod -aG party bob
.Ed
.Pp
Have alice and bob log out and back in so the new group takes effect.
.Pp
Host a party named
.Ql debug-the-deploy
and invite alice as a watcher and bob as a co-driver:
.Bd -literal -offset indent
$ party host debug-the-deploy
$ party invite alice -r
$ party invite bob
.Ed
.Pp
On bob's side, join the only running party on this host:
.Bd -literal -offset indent
$ party join
.Ed
.Pp
On alice's side, join read-only:
.Bd -literal -offset indent
$ party join debug-the-deploy --passive
.Ed
.Pp
Reuse an existing group instead of creating a new one:
.Bd -literal -offset indent
$ party host debug-the-deploy --group staff
.Ed
.Sh SECURITY
The OS group is the perimeter,
.Xr tmux 1 Ns 's
.Cm server-access
allowlist is the per-user filter.
Both are universal across the supported platforms (Linux, FreeBSD,
NetBSD, OpenBSD, illumos, Solaris, macOS), so the mechanism is one
liner:
.Xr chgrp 1
+
.Xr chmod 1 .
No ACL syscalls are invoked.
.Pp
Trust is anchored in unforgeable primitives:
.Xr mkdir 2
and
.Xr bind 2
set the owner to the calling effective UID, and
.Xr chown 1
requires root.
A group member who plants a forged directory cannot fake its
ownership, cannot redirect the socket path, and cannot forge a socket
elsewhere.
.Pp
.Nm
is designed for small, mutually trusted groups: a hacklab, a tech
team, a circle of friends, not strangers across the internet.
It assumes you already know and trust the people you are adding to
the group; it does not try to be a public access-control system.
.Sh CAVEATS
On filesystems with NFSv4-style inherited ACLs (macOS HFS+/APFS,
FreeBSD/illumos ZFS), an ACE on
.Ev PARTY_SOCKET_DIR
can override the mode bits
.Nm
sets, widening access (e.g.\& an inherited
.Ql everyone@:rx
survives our
.Ql chmod 0750 )
or narrowing it (e.g.\& a
.Ql deny everyone:
blocks group members our
.Xr chgrp 1
tried to admit).
The script does not invoke
.Xr getfacl 1
or
.Xr getextattr 8
to verify effective access; re-introducing per-platform ACL plumbing
would unwind the simplification this design commits to.
The FS gate is therefore best-effort on those filesystems; the
auth gate
.Po
.Xr tmux 1
.Cm server-access
.Pc
remains authoritative regardless.
The default
.Ev PARTY_SOCKET_DIR Ns = Ns Pa /tmp
rarely has ACL trouble in practice; if you deploy on a custom dir
with restrictive inherited ACLs, expect to handle traversal at the
ACL layer for your intended group members.
Strangers stay out via
.Cm server-access
either way.
.Pp
.Cm server-access
decides who may
.Em attach ,
not who can see that a party
.Em exists .
Hiding a party's existence, meaning the host, its name, group, and
creation time recorded in the
.Pa roster ,
falls to the FS gate, not the auth gate: on an ACL-shadowed dir where the
mode bits do not hold, a non-member may read that metadata even though
.Cm server-access
still blocks the join.
The
.Cm list
subcommand re-checks group membership before printing, as defense in
depth, but treat discovery as confidential only insofar as the filesystem
enforces it.
.Pp
An active
.Cm join
lands you in your own
.Ql __party_guest_<you>
session on the shared server.
Any write-capable invitee can create sessions there, including one named
for someone else; if they pre-create yours, your next active
.Cm join
attaches to it.
This is inside the trust boundary by design, not a defended one;
invitees are people you already trust.
If you do not trust an invitee that far, invite them read-only
.Pq Fl r ,
which cannot create sessions, or not at all.
.Pp
.Nm
requires
.Xr tmux 1
\(>= 3.3 for
.Cm server-access .
Older
.Xr tmux 1
versions are refused at host time rather than degrading to the FS
gate alone.
.Pp
The
.Cm role
subcommand can flip a client between the guest session and the host
session, but it cannot change the read-only state of an attached
client;
.Xr tmux 1
.Cm switch-client
has no equivalent of
.Cm attach Fl r .
The watcher boundary is set at
.Cm join Fl -passive
time and is fixed for the life of that client.
.Sh SEE ALSO
.Xr tmux 1 ,
.Xr screen 1 ,
.Xr write 1 ,
.Xr chgrp 1 ,
.Xr chmod 1
.Sh HISTORY
.Nm
was written for the UNIX Social Club
.Pq Lk https://club.unix.rocks/ ,
inspired by many years of collective working sessions using
.Ql screen -x
across collectives.
.Sh AUTHORS
.Nm
was written by
.An veg
and
.An kol3rby .
See the
.Pa party
script header for additional credits.
Issues and contributions welcome!