Run Omarchy sessions as windows inside the one you are using. No VM, no
reboot, no omarchy dev link: each nest is a nested Hyprland with its own
omarchy-shell, pointed at whatever Omarchy source tree you choose. Up to nine
run side by side in slots 1-9, each with its own checkout if you like.
Because it is the same kernel and the same user, plugins in the nest see the
real hardware: /sys/class/power_supply, RAPL power limits, the DDC i2c bus,
hwmon, the system bus. Only the compositor and the session bus are separate.
Six sessions on the omadev workspace, each a full Omarchy with its own bar; the middle one is capturing keys.
Install the widget through Omarchy's plugin manager:
omarchy plugin add https://github.com/llstrk/omadev --enableThe repository-root manifest points to plugin/dry.omadev/Omadev.qml. Loading the
widget only probes for a backend. If none is available, the panel offers Install
Omadev backend. A click opens a terminal and requires confirmation before building
or changing host bindings. A compiler, pkg-config and Wayland 1.26+ development files
must already be installed. No package-manager or privileged operation runs automatically.
The button installs an independent backend snapshot under
~/.local/share/omadev/backends/, links its launcher into ~/.local/bin, and runs
omadev setup. Plugin updates/removal cannot change or remove that backend. If an old
or incompatible backend is found, the panel offers a compatible source installation;
update the plugin first to get current backend sources. Existing backend snapshots
are kept for rollback and may be removed manually once no session uses them.
Alternatively, install the backend package and run omadev setup, or run
./install.sh from a separate development checkout. The latter intentionally tracks
that checkout. Running it from an installed plugin instead takes the independent
snapshot/confirmation path. The package recipe is backend-only; a release archive
containing this protocol still needs to be published before the package can use it.
omadev setup now checks for the required native helper, configures host bindings,
links agent skills, and writes ~/.config/omadev/backend.json. It never copies,
rewrites, enables, or deletes the widget. The widget uses that registration (then
~/.local/bin, then PATH) and checks omadev capabilities --json; nested widgets pin
their own session backend. An incompatible backend is never used for capture.
Update the widget with omarchy plugin update dry.omadev. Backend installation and
host setup remain explicit actions. See installation ownership and migration.
The bindings omadev setup installs in ~/.config/hypr/bindings.lua:
- Super+Alt+D opens the sessions panel, or releases captured input through the absolute path to this launcher. It does not depend on the compositor finding the right
omadevon PATH. - Super+Shift+Alt+D, while captured, is an explicit emergency escape. The compositor resets routing directly and attempts a warning notification. This bypasses key-cleanup acknowledgement; stop the nest if keys remain held.
Normal release failures notify rather than silently bypassing cleanup. Run setup to generate the bindings for your installation path, and restart existing nests after upgrading.
The bar widget dry.omadev (in plugin/) is the visual side. It must be a
real directory under ~/.config/omarchy/plugins: the shell watches that tree
with inotifywait -r, which does not descend into symlinked directories, so a
symlinked plugin never hot-reloads. The plugin manager owns the real git checkout. On the host it is one icon,
lit while sessions run, that opens a panel of the open sessions: a row per
session with focus-and-capture and stop actions, and a + button that
starts a new session in the first free slot. The panel takes the keyboard:
j/k move, Enter focuses and captures, and 1–9 focus and capture the corresponding
session. While the panel is open, each visible session window has its number in
the center. Opening the panel places the outline without flashing. Selecting a row
moves the outline to its visible host window, then flashes that window without
changing focus. The outline, flash and numbers use the theme's magenta palette color. Number shortcuts wait for the outline to arrive before focusing and closing the
panel, then play only the session's red capture pulse (no extra selection flash).
n starts a session, d (or x) marks the selected session red, and a second press
confirms stopping it. The row immediately shows “Stopping…” and selection advances
to the next running session (or the previous one at the end). Navigation skips
unavailable rows and keeps the same session selected as stopped rows disappear.
Moving selection or closing cancels confirmation.
w goes to the omadev workspace, Esc closes. It also closes
when it loses keyboard focus, for instance on a workspace switch, except
right after n: a session's window mapping makes the compositor take focus
away for a moment, and the panel takes it back, whoever started the session. Inside a session it shows
whether the host is passing keys there and releases them on click. A full-window
pulse uses theme red when capture starts and theme magenta when keys are released; while
keys are captured a small box under the widget says how to release them. That label
shrinks with the session's width so the bar's centre block does not run into
it when the window is tiled narrow: full text, then a single word, then only
the icon and slot number (the red highlight still shows capture). A read-only
modifier readout remains visible at every size: mods: none, mods: Super+Ctrl,
or mods: ? when unavailable. A larger box below the release hint shows individual
modifier labels, with held keys highlighted in the theme color. The box remains
visible after release if any modifiers still appear held. It reports the nested compositor's key-down state
for Super, Ctrl, Alt, Shift and AltGr, not the host's physical keyboard or an
app's internal modifier state. In legacy (ungated) sessions, when capture starts, the widget takes a fresh
snapshot and sends key-up events for reported held modifier keys to that nest,
after verifying it still has host focus and capture. It sends no key-downs and
does not keep clearing modifiers during normal typing. Capture/release timing
is unchanged; the displayed labels themselves remain read-only. The two thresholds are widget settings, compactBelow (1300) and iconOnlyBelow (900). A new
plugin id needs one omarchy restart shell before the bar picks it up.
A new session's window opens on the host workspace named omadev, shared by
all sessions, without switching to it or taking focus from what you were
doing. The launcher installs a window rule for that into the running host
compositor (class aquamarine, floating, no initial focus, silent workspace,
render-unfocused so a hidden nest keeps getting frames)
through hyprctl eval; nothing is written to your Hyprland config, and a
host hyprctl reload drops the rule until the next start re-adds it. There the sessions are laid out as an even grid of
floating windows, recomputed whenever one starts or stops (dwindle would keep
halving whichever window was focused last). Press w in the sessions panel to go to that workspace,
or jump to one session from its row or with omadev focus N; drag it back
into a tiled layout whenever you like. --place tile (or OMADEV_PLACE=tile)
tiles the window on the workspace you were on when you started it instead.
Closing a session's window on the host ends the session: the launcher watches for its window and asks the nested compositor to exit when it is gone, since Hyprland itself keeps running without an output.
omadev # lowest free slot, installed /usr/share/omarchy
omadev start 2 --path ~/Projects/omarchy # slot 2 runs a checkout
omadev --scale 1.25 # HiDPI nest; the size always follows the window
omadev --no-shell # compositor only
omadev start 2 # host keys blocked until capture by default; automation still works
omadev list # the nine slots
omadev start --detach # return once the nest's shell answers (start otherwise blocks)
omadev start --force --place tile # opt in to starting a child inside a nest
omadev log 2 -f # follow nest 2's shell and app output (not the compositor's)start without --detach stays in the foreground for the life of the nest,
like running Hyprland by hand; Ctrl-C ends it, compositor and all. Scripts and
agents can use ensure or --detach to launch the nest without blocking. That
only detaches the session launcher: substantial tasks inside the nest should
still run in visible foreground apps or terminals. omarchy nested ... is not routed by the Omarchy CLI, which only
discovers commands in its own bin directory; call omadev directly.
Super+Alt+D opens the sessions panel; each row focuses its nest and captures
keys for it. Super+Alt+D releases captured keys. The keyboard icon on a panel
row focuses and captures that session; the icon inside a session toggles capture.
Capture focuses the window and verifies focus in a separate compositor round trip
before entering passthrough. omadev release waits for any gated session to release forwarded keys before resetting the host submap. With only legacy sessions, it resets routing directly.
Capture is for the person at the keyboard: omadev focus refuses unless
OMADEV_ALLOW_CAPTURE=1 is explicitly set. The widget sets it for user actions
and offers no capture method over its IPC interface. Ctrl-C in the launching terminal or omadev stop N ends a
nest; omadev stop all ends every nest. stop waits until the slot
is free (up to 15 s; --kill then sends SIGTERM to a compositor that ignores
the exit request) and returns once the slot can be started again. Stopping
ends everything that carries the nest's environment, including apps that
detached from the compositor and commands started with omadev run; a nest
that is still starting can be stopped too. list shows
a nest as starting until it has published its record and as stopping on
the way out. Each slot's launcher.log stays under the runtime directory for
post-mortems.
Starting several nests at once is safe: the launcher holds a lock on the slot
(a flock, released only when the launcher exits, so it is never stale) before
anything is launched, so concurrent starts without a slot number get distinct
slots and two starts of the same number leave one of them refused.
The nested compositor occasionally stalls in its handshake with the host (its log stops while listing buffer formats, it never gets an output, and nothing inside starts). The launcher treats a nest that has not published its record within 15 s as stalled, kills that compositor tree and launches again, up to three times, so a start either yields a working nest or fails loudly; it never leaves a headless compositor behind.
A slot is alive when its compositor socket ($XDG_RUNTIME_DIR/hypr/<sig>/)
accepts a connection; the recorded PID is only a fallback. A session record
that can be neither reached nor verified, as inside an agent sandbox with its own PID
namespace or without the runtime directory, lists as unreachable rather than
free, and only the launcher that wrote a record ever removes it.
From any host terminal you can drive a nest without focusing it. The slot number is optional when only one nest runs or when you are inside one:
omadev run 2 omarchy restart shell # restart only nest 2's shell
omadev shell 2 shell listPlugins # omarchy-shell IPC in nest 2
omadev hyprctl 2 clients # hyprctl against nest 2
omadev hyprctl 2 dispatch 'hl.dsp.exec_cmd("ghostty")'
omadev run 2 wtype "text" # type into the focused app (compositor binds do not fire)
omadev list --json # every nest's environment, owner, pid; state free|starting|running|stopping|unreachable
OMADEV_ALLOW_CAPTURE=1 omadev focus 2 # focus nest 2 and capture keys (user-only guard); no number: the focused nestNested-only Hyprland config goes in ~/.config/hypr/omadev.lua; it is copied into each new nest and loaded after the normal config. Edit the nest's private copy for live changes, or start a fresh nest to pick up host edits.
Every new session blocks host keyboard events before Hyprland processes them, while preserving virtual test input. Capture waits for pre-held keys to be released. Focus loss or controller failure revokes capture; merely returning focus cannot recapture. The nested widget reports the actual gate state, including RELEASE HELD KEYS and INPUT STATUS UNKNOWN. Unknown status is not a guarantee that input is blocked.
Build with ./bin/build-input-gate, or use the installer/package build. Human capture requires the updated omadev setup release binding. omadev input-status N and list --json expose readiness/state. Startup refuses to silently run unguarded if the native helper fails to initialize. There is no legacy fallback for new sessions. Existing ungated sessions must be restarted. See input gate design, tests, and rollout.
A nest never shares Omarchy config with the host or with other nests. Its
home under ~/.local/state/omadev/N/home is a directory of symlinks to the
real home, except:
~/.config/omarchy,~/.config/hyprand~/.local/state/omarchy, which are reflink clones (btrfs copy-on-write: instant, and no blocks of their own until a file is written). shell.json, plugins, themes and toggles in the nest are the nest's. A config directory that is itself a symlink (a dotfiles checkout) is copied as a directory, so the nest never writes through the link.- Chromium-family browser profiles (
~/.config/chromiumand friends), which start empty: those browsers are single-instance per profile, so through a linked profile a browser started in the nest would just open a window in the one already running on the host.
Everything else, projects, keys, app configs, is the real file. Symlinks
inside the cloned trees are kept private too: one pointing elsewhere in
the same tree is re-pointed into the clone, one pointing outside it (a
shell.json from a dotfiles checkout) becomes a copy. The nest also gets its
own .bashrc and login profile, which select the checkout before running yours and restore checkout/PATH ordering afterwards. A Bash-only source wrapper redirects Omarchy's environment bootstrap to the nest's selection, so aliases and functions load from the checkout rather than the host. BASH_ENV applies the same selection to non-interactive Bash scripts. No global bootstrap or /etc/omarchy.conf is modified.
The nest's XDG_RUNTIME_DIR is private in the same way: $XDG_RUNTIME_DIR/omadev-N
(short on purpose: Unix socket paths are capped at 107 bytes and the host
compositor's control socket is reached through it), where the host's sockets and the directories that hold
them (Wayland, PipeWire, PulseAudio, GnuPG and SSH agents, keyring, hypr/
with every compositor's control sockets) are symlinks, and directories of plain
files start empty. Per-session state such as a screen recorder's "already
recording" file, dconf, or the quickshell instance list is therefore the
nest's own: omareel on the host and omareel in a nest no longer see each other.
omadev start 3 --detach --plugin ~/worktrees/dell-monitor-fix
omadev run 3 omarchy plugin enable dry.dell-monitor # only nest 3's shell.json changes
omadev diff 3 # what the nest changed
omadev stop 3 && omadev clean 3 # the home is kept until clean--plugin <dir> links a checkout, typically a git worktree, into the nest's
plugins directory under its manifest id, so each teammate works on its own
branch and the orchestrator merges branches instead of copying files. It also
works through ensure on a running nest. --reuse keeps the previous home of
a slot instead of cloning a fresh one. Reusing a home from an older release privatizes its previously shared Hyprland directory first. diff includes Hyprland config but excludes linked checkouts (use git there) and churny state such as clipboard history.
--path selects checkout code and defaults, not a fresh install: existing user config and generated theme state are copied. A valid user shell.json replaces the checkout's default shell config entirely. The installed Hyprland and Quickshell binaries are still used.
At startup, omadev sources the checkout's default/uwsm/default, followed by the private-home view of ~/.config/uwsm/default. It preserves added, changed, empty and unset exported variables for commands run through omadev run. It does not run UWSM itself or its full env.d startup chain. Bash startup is checkout-aware; other interactive shells' own startup files are not rewritten.
The shell and checkout commands inherit PATH in this order: omadev safety overlays, the selected checkout's bin, then the remaining inherited paths. Changes to shell code need omadev run N omarchy restart shell; use omadev hyprctl N reload for Hyprland config changes.
Known host-affecting commands are guarded:
sudo,pkexec,systemctl, broad app/service restart helpers and app/hardware theme helpers are refused inside a nest. Run intentional host operations from a host terminal.omarchy refresh configonly accepts paths inside privatehypr/oromarchy/config, rejecting traversal and symlinks leading outside those trees.omarchy refresh hyprlandnow changes only the nest.omarchy theme setbuilds the private theme using the stock headless mode, then reloads only the nest's compositor and shell. It skips post-theme hooks, app config writes, terminal signals and keyboard lighting. The nested desktop updates, but there is no animated theme transition or host-app retinting.
These are guardrails, not a security sandbox. Projects, keys, most app config, cache and data directories, host sockets, processes and hardware remain shared. Absolute executable paths, direct file writes, custom scripts and direct D-Bus/hardware access can bypass the guards. Only run trusted checkouts and plugins.
python3 -m unittest discover -s tests -p 'test_*.py' -v
node tests/panel.cjsThe checkout tests use disposable homes and harmless command stubs. The router integration test also exercises the installed Omarchy router if available. They require neither a compositor nor root privileges.
skill/SKILL.md (linked into the agent skill directories by the installer) is
the agent-facing guide.
Prefer work the user can see. Use the actual app for UI work, and run builds, tests, servers and other substantial tasks in a visible terminal inside the session. Keep progress and results visible instead of hiding the main work in headless commands, detached tmux sessions, background jobs or logs alone. Quick probes, IPC, readiness checks and genuinely background services can stay headless; explicit user requests and technical requirements can override the preference. Do not take host focus, capture the user's keyboard or switch their workspace just to make the work visible.
For example (replace the project path):
omadev hyprctl 3 dispatch \
'hl.dsp.exec_cmd("ghostty --title=omadev-3-build-tests --wait-after-command=true -e bash -lc \"cd ~/Work/my-project && ./bin/build && ./bin/test\"")'The terminal is launched asynchronously, but the task runs in its foreground.
--wait-after-command leaves the output visible when it finishes.
Other commands for driving the nest:
omadev ensure 3 --owner teammate-1 --plugin ~/wt/fix # idempotent claim, waits for the shell, JSON out
omadev list --json # slots, owners, pids, paths, environments
omadev screenshot 3 [file] # grim inside the nest
omadev run 3 wtype "text" # into the focused app, no host focus needed
omadev hyprctl 3 dispatch 'hl.dsp.exec_cmd("ghostty")' # compositor-level actions| Nest | |
|---|---|
~/.config/omarchy (shell.json, plugins, themes, current theme) |
private reflink clone per nest |
~/.config/hypr/*.lua |
private reflink clone per nest |
| Hardware via sysfs, hwmon, i2c, system bus | shared, real values |
OMARCHY_PATH |
the nest's own (--path) |
| Compositor, Wayland socket, Hyprland instance | separate |
| Session bus (notifications, tray, MPRIS) | private to the nest by default |
XDG_RUNTIME_DIR |
private; host sockets (Wayland, PipeWire, agents, hypr/) linked in |
| systemd user units, portals, fcitx5, polkit agent | host only |
- Autostart. Omarchy's session autostart is skipped in the nest. It would
import the nest's
WAYLAND_DISPLAYinto the systemd user manager, start a second udiskie and reset power profiles. The nest runs only the shell, an idle inhibitor and a small script that publishes its environment. omarchy restart shell. The stock command takes the host'sOMARCHY_PATHfrom the systemd environment and kills every quickshell running that config on any display, which is the host bar.libexec/overlay/shadows it with a version that only touches the nest's display. The checkout's own router still handles argument parsing, aliases and help, but its final dispatch honors overlays, including foromarchy restart-shell.uwsm-appandsystemd-run. The real ones hand commands towayland-wm-app-daemonor the user's systemd manager, which spawn them with the host's environment, so SUPER+Return would open a terminal on the host and SUPER+B a browser. The overlays run the command directly in the nest instead.- Idle and lock. The launcher disables the shell's automatic screensaver
and lock cycle using a stay-awake marker in the nest's private state, including
reused homes. The host's idle settings are untouched. A small Quickshell
"keeper" also holds a Wayland idle inhibitor as an extra safeguard, but idle
prevention does not depend on that surface remaining visible. The session
widget also disables idle at startup (retrying until the idle service is ready),
so sessions created by an older installed launcher are protected too.
--idleskips this enforcement, clears the private marker in the current launcher, and turns the inhibitor off for testing idle behavior. - Window size. The nested output follows the host window. Hyprland's Wayland backend resizes the output, but the compositor neither re-arranges its layers and windows nor tells clients about the new mode until the monitor rule is re-applied. The keeper polls the output size twice a second and re-applies the rule on a change, so bar, background and windows follow within about a second. There is no fixed-size mode.
- Logs. The
systemd-catshim retags the nested shell's journal output asomadev-N, soomadev log Nreads one nest without PID archaeology. The compositor's own log stays in its runtime directory. - Session bus.
dbus-run-sessiongives the nest a private bus so the nested shell can ownorg.freedesktop.Notifications. The bus uses a generated config whose service directory omits the portal and at-spi services: the Hyprland portal segfaults against a nested compositor and at-spi loops without systemd, and the host provides both anyway. The activation environment is pointed at the nest, so anything else it activates connects to the nested compositor.
- The nested compositor has no DRM outputs. Monitor hotplug, DPMS, lid handling, hyprsunset and clamshell logic cannot be tested here.
- Anything that writes hardware state writes the real hardware. Keep write paths behind a dry-run flag while iterating.
- Commands that touch systemd user units or the package system (
omarchy update,omarchy dev link) act on the host. Do not run them in the nest. - A nest whose window is on a workspace you are not looking at keeps
rendering, at the host's
misc.render_unfocused_fps(15 by default): the window rule the launcher installs marks nest windows render-unfocused, so screenshots and GUI tests inside a hidden nest work. Whether that also holds while the host is locked is not verified. - Every nest logs a few warnings because the host already owns the
session-wide service: portal app-ID registration, the polkit agent, at-spi,
and Quickshell's duplicate-IPC-handler notice for user plugins.
omadev log Nhides them;--allshows them. - No portals inside a nest: portal file dialogs and screen sharing fall back or fail there. Apps still run; grim screenshots still work.
- The host tray does not show apps running in the nest, and host notifications do not appear in the nest.
- Nine slots, each with its own copy of
~/.config/omarchy: a host plugin edit reaches a nest only when the nest is restarted (or the plugin is linked). - The host cannot tell nest windows apart by title; the widget and the
focuscommand find them by the compositor's PID.
bin/omadev launcher and every control command (Python 3, standard library only);
its hidden `_publish` runs inside the nest on start to record the environment;
`setup` checks/registers the backend, configures bindings and links skills
manifest.json root manifest for omarchy plugin add
plugin/dry.omadev/ widget and its read-only backend probe/confirmed installer
pkg/ PKGBUILD and install script for the Omarchy package repository
libexec/hyprland.lua nested Hyprland config (wraps the private copy of your hyprland.lua)
libexec/bash-env checkout-aware Bash bootstrap, used by BASH_ENV and private startup files
libexec/overlay/ CLI dispatch, safe refresh/theme/restart, launcher shims and host-command guards
libexec/refuse-host-command refusal script mirrored as regular guard files (plugin repositories cannot contain symlinks)
libexec/keeper/shell.qml per-nest helper: idle inhibitor + follow-the-window resize
libexec/input_gate.py launcher-owned controller for keyboard isolation
libexec/input-gate/ native host-Wayland keyboard filter, built by bin/build-input-gate
tests/ disposable checkout/environment tests and panel behavior tests
