clonetty
Spawn a new Alacritty window that clones an existing
terminal to form a new instance at the same working directory, and by default
its full environment reconstructed as a stack of nested shells (so Ctrl-D
peels back one nix-shell/subshell layer at a time, just like the original).
Optionally, collapse the stack to just the active environment.
This is perhaps of most interest to tiling windows manager users and those who frequently work in the terminal with multipl instances.
Future Directions
The involved parts include the
- graphical display system (Xorg or Wayland),
- desktop environment,
- terminal, and
- shell. This proejct focuses on the terminal first, however X11 offers a window environment variable that can be useful to generalize across terminals.
This has been confirmed to work with alacritty using X11's focused window environment variable. That should generalize the aquisition of the focused terminal's PID, and opens up clonetty's use for other terminals in the future.
After alacritty, the following terminals will slowly be included if a general approach is unable to adopted:
- kitty
- ghostty
- gnome-terminal (targeting the downstream one default in Ubuntu. It has an environment variable that upstream removed)
- Konsole
How it works
clonetty walks the process tree from the source shell up to the hosting
alacritty, reading each ancestor shell's environment from /proc
(comm, status for PPid:, environ). Adjacent layers with an identical
environment are collapsed, leaving one entry per distinct environment layer —
outermost (login) shell first, innermost (leaf) shell last.
-
Current terminal (default): walked from this process's parent; cwd is this process's cwd.
-
Targeted terminal (
--pid): walked from/proc/<pid>/. Give the innermost shell PID (echo $$in the target window) to reconstruct nesting to full depth; the alacritty PID only resolves to its outermost child shell.Reading another process requires the same UID (fine for your own terminals).
-
Alacritty is one process, many windows. So the alacritty PID can't name a single window; the per-window handle is the shell PID inside it. If you pass the alacritty PID,
clonettydescends to its child shell — and if there are several windows, it lists them (with cwd) so you can pick the shell PID.
This walk is read-only: it only reads /proc and never signals or writes to
the running shells, so cloning a terminal can never exit, disturb, or alter the
source terminal's shell stack.
Reconstruction (default): env-delta nested shells
The new window is launched with the base login-shell environment (level 0,
which predates any nix-shell — it has the session vars Alacritty needs, e.g.
DISPLAY/WAYLAND_DISPLAY, XAUTHORITY, XDG_RUNTIME_DIR, DBUS_*, HOME,
PATH, and none of the nix vars). Alacritty then runs a chain of nested
bash shells, one per captured layer, via synthesized rc files:
- Each rc file applies only that layer's env delta (
exportthe vars it added/changed,unsetthe ones it removed) — the vars anix-shell(or any subshell) contributed on top of the layer below. - Each rc file then runs (not
execs) the next shell as a child. Because control returns to the parent shell when its child exits,Ctrl-Dpeels one layer at a time to arbitrary depth. The finalCtrl-Dat the base shell closes the window.
The rc files are written to a private (0700) temp dir with 0600 perms (they
may hold secrets from the copied environment) and are deleted by the new
window's base shell on exit. Use --dry-run to print them without writing or
spawning.
Window-/session-specific variables (ALACRITTY_*, TERM, PWD, OLDPWD,
SHLVL, WINDOWID, TMUX*, STY, ...) are scrubbed from every layer so the
new Alacritty and shells set their own correct values.
--base: cull to a clean terminal
--base skips reconstruction and opens a fresh terminal with just the outermost
login-shell environment at the same path — no nix-shell/subshell nesting.
--reuse
--reuse (alacritty msg create-window) opens the window via the running
Alacritty's IPC socket. It's faster but the window inherits that process's
environment, so neither a custom environment nor nested-shell reconstruction is
possible in this mode.
Caveats
- Reconstruction assumes
bashfor the shell layers (matchesnix-shell). - Each layer re-sources
~/.bashrcfor interactive niceties (prompt, aliases) and then applies its env delta authoritatively; a~/.bashrcthat mutates env non-idempotently (e.g. unconditionally prepends toPATH) may duplicate such entries. Env vars that changed between layers are always reconstructed exactly. - Exported shell functions (
BASH_FUNC_*%%) and other non-identifier keys are skipped rather than replayed as broken assignments.
Alacritty Keybinding
Add to alacritty.toml to clone the focused window with a keybinding:
[]
= [
{ = "N", = "Control|Shift", = { = "clonetty" } },
]
Keybindings on Wayland: let your compositor supply --pid
--focused is Xorg-only: it reads _NET_ACTIVE_WINDOW → _NET_WM_PID off the
X root window. Wayland has no portable equivalent — client isolation forbids
querying another client's window, and PID is deliberately absent from the
wlr-foreign-toplevel-management
and ext-foreign-toplevel-list
protocols (open request: wlr-protocols#76).
But the compositor knows the focused window, and your keybinding already lives
there. So resolve the focused window's PID with the compositor's own IPC and pass
it to clonetty --pid. This keeps clonetty windowing-agnostic (--focused is
just a bundled X11 convenience backend); each environment plugs in its native
resolver. Every snippet below needs jq and the compositor CLI on PATH.
Sway (sway IPC) — ~/.config/sway/config:
bindsym $mod+Shift+Return exec clonetty --pid "$(swaymsg -t get_tree | jq '.. | select(.focused? == true).pid')"
Hyprland — hyprland.conf (bind syntax):
bind = $mainMod SHIFT, Return, exec, clonetty --pid $(hyprctl activewindow -j | jq .pid)
niri — config.kdl (niri's spawn is shell-less, so wrap in sh -c):
binds {
Mod+Shift+Return { spawn "sh" "-c" "clonetty --pid $(niri msg --json focused-window | jq .pid)"; }
}
KDE Plasma / KWin — KWin exposes no
xdotool-style PID query, but its scripting API has workspace.activeWindow.pid.
Drive it with kdotool (it loads a KWin script
under the hood) or a focus-tracking script like
FocusNotifier, then bind a Custom
Shortcut (System Settings → Shortcuts) that reads the PID and calls
clonetty --pid.
GNOME Shell (Wayland) — GNOME exposes no window
API to clients; install a D-Bus extension that publishes the focused window's PID,
e.g. focused-window-dbus or
window-calls-extended (adds a
FocusPID/List method). Bind a Custom Shortcut (Settings → Keyboard) to a small
sh -c that gdbus calls the extension's method and passes the PID to
clonetty --pid (exact object path/method are in the extension's README).
COSMIC (System76) — no focused-PID
resolver exists on current COSMIC (Epoch 1.4): the
cosmic-toplevel-info-v1
protocol (v3) and the community
cosmic-ext-window-helper
expose app_id/title/state but not PID; native window management is slated
for COSMIC Epoch 2. Two options today:
- XWayland bridge (test first): run the terminal under XWayland
(
WINIT_UNIX_BACKEND=x11 alacritty) and useclonetty --focused, which reads XWayland's_NET_ACTIVE_WINDOW/_NET_WM_PID. Confirm cosmic-comp maintains them —xprop -root _NET_ACTIVE_WINDOWandxdotool getactivewindow getwindowpidshould resolve to the focused terminal. Only covers XWayland (X11) windows. - No bridge: run
clonettyin-terminal (no--pid; full fidelity) orclonetty --pid "$$"from within the target shell.
Others — Wayfire exposes a
focused-view query via its ipc plugin; river
has no window-query API (it is tag-based), so there you'd run a
wlr-foreign-toplevel client and match by app-id/title — PID isn't in that protocol
(#76), a genuine gap until it is added.
Graceful degradation: where no focused-PID resolver exists yet (COSMIC, river), the in-terminal
clonetty(no flag) and--pid "$$"paths always work and are the highest fidelity — the keybinding is only a convenience for capturing another window without focusing a shell prompt first.