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.
435 lines
9.8 KiB
Groff
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!
|