Omahub
← All plugins
W

Recently Closed

by Weaklund

Reopen a window you just closed — back on its workspace, in its directory, running what it ran.

Security review

No obvious issues detected

Deterministic scan — not a security guarantee

None
Risk level
None
Analyzed commit
413dc71
Scanned
1 month ago

No potentially dangerous behavior detected in the analyzed commit.

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
413dc71
Reviewed
1 month ago

The deterministic scan found no issues, and the manual review found no malicious code: no install-time hooks, no network access, no privilege escalation, and the state file is opened and bounded-read defensively (O_NOFOLLOW, 0600, 1 MiB cap). The only meaningful risk is the feature itself—recording and later re-executing user command lines and browser URLs—which is disclosed in the README and handled with careful shell quoting and PlainText rendering.

  • The service stores full command lines of every window (including terminal commands that may contain secrets) and recovered browser tab URLs in ~/.local/state/omarchy/recently-closed.json; the file is owner-only and bounded, but the data is inherently sensitive.
  • Reopening replays captured command lines through a shell; the code quotes arguments with printf %q and tests the round-trip, so no injection was found, but this is the critical surface to keep in mind if the plugin is modified.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/monswiklund/omarchy-recently-closed --enable
Productivity #bar #workspaces

Recently Closed

Reopen a window you just closed — back on its workspace, in its directory, running what it was running. An Omarchy Quattro bar widget.

Requirements

Needed for What
Everything Omarchy Quattro — the shell plugin system does not exist before it
Private history python3, for descriptor-safe bounded reads
Capturing hyprctl and jq, both already on an Omarchy install
Browser tabs The same python3; without recovered tabs browsers come back empty

No other dependencies, nothing is downloaded at runtime, and no privilege escalation is used. The plugin writes one owner-only (0600) file of its own, ~/.local/state/omarchy/recently-closed.json, and edits nothing else you own.

Terminal support is a small table: ghostty, foot and alacritty are verified. An unrecognised terminal is still remembered, it just comes back without its directory rather than with flags that would break it.

Install

omarchy plugin add https://github.com/monswiklund/omarchy-recently-closed.git --enable

It lands on the left of the bar. Somewhere else:

omarchy plugin enable io.github.monswiklund.recently-closed --section right

How many windows it remembers — 3 to 40, 12 by default — and the bar icon are widget settings, edited where every other widget's are.

Installed by hand instead:

omarchy-shell shell rescanPlugins
omarchy plugin enable io.github.monswiklund.recently-closed

How it can possibly know

Hyprland's close event carries nothing but an address:

openwindow>>563fe6dc84a0,3,com.mitchellh.ghostty,Ghostty
closewindow>>563fe6dc84a0

By the time that arrives the window's process is gone, and /proc has nothing left to read. So nothing is worked out after the fact. A service watches windows open, describes each one while it is alive, and keeps the description until the window closes — at which point the description is all that is left of it.

Three things that description gets right, each of which took a bug to find:

  • A window is not itself yet when it opens. A terminal exists before the shell inside it does, so reading at that instant finds an empty box and records a command with no directory. Capture waits a moment first.
  • Only a terminal has a session inside it. Every other app has child processes too — Electron keeps a pile — and treating those as "what the window was running" turns Spotify into a command line full of zygote helpers.
  • Hyprland names a window two ways. The event socket writes 563f… and hyprctl clients writes 0x563f…. Everything here speaks the prefixed form, because a close event that cannot find what its own open event stored fails silently and leaves the list mysteriously empty.

A window's directory moves under it — you open a terminal in home and cd into a project — so the focused window is re-read every 20 seconds. Slow on purpose: it is a process spawn, and the only window whose directory changes is the one being typed in.

The list

The screensaver is not in it. Some windows are not closed so much as finished: the screensaver dismisses itself the moment you touch the keyboard, and offering to bring it back is offering to blank the screen you just unblanked. Omarchy's other own windows — btop, a terminal, the about box — are windows you might genuinely want back, so the ignore list stays one line long.

Newest first, one row per window closed. Two terminals closed in the same directory are two rows, because they were two windows and getting one back does not give you the other — a rule here once collapsed them and the second was lost for good.

Nothing needs deduplicating: a window closes once, and a row is removed as it reopens, so closing and reopening the same thing never accumulates.

A row is removed as it reopens. It is not closed any more, and a row that reopens the same window twice lies the second time.

Reopen where it was Click a row, or ↑/↓ and Enter
Reopen here Click the workspace badge on the row
Forget everything "Clear the list"
How many to keep Widget setting, 3 to 40, default 12

A closed window usually wants to come back where it was, but not always: after you have moved on, the workspace you are standing in is the one you meant. The badge already says which workspace the window came from, so it is the honest place to say not that one, this one — and it is the only way to rescue a window closed on a scratchpad, which otherwise comes back nowhere in particular.

Tiled windows come back on their workspace and take their place from the layout; floating ones come back placed, because position and size are rules Hyprland accepts on the exec itself. A window closed on a special workspace — a scratchpad — comes back without one.

From a keybinding

Undoing the last close without looking is the point:

omarchy-shell recently-closed reopen    # ok / empty
omarchy-shell recently-closed toggle    # the panel

In ~/.config/hypr/bindings.lua:

o.bind("SUPER + SHIFT + T", "Reopen last closed window",
  hl.dsp.exec_cmd("omarchy-shell recently-closed reopen"))

Browser windows come back whole

A browser keeps no URL on its command line, so a window rebuilt from /proc alone comes back empty. It does write its session to disk, and that file says which tabs belong to which window — so every tab returns, in order, in one window.

scripts/browser-tabs reads Chromium's SNSS session format directly: a header followed by length-prefixed commands, of which two matter — one binds a tab to a window, the other carries a tab's URL and title. Anything unrecognised is skipped by its own length, so a version that adds commands still parses.

The window is found by its title, which is the title of the tab you were looking at. Several windows can share a title — three tabs on the same site — so the most recently touched one wins, since the session writes the newest last.

Chromium, Chrome, Brave, Edge and Vivaldi share this format. Firefox does not and comes back empty.

A window's command line is not trusted

What a window was running is replayed through hl.dsp.exec_cmd, which hands a string to a shell. A window's command line is full of things somebody else chose — filenames, URLs, profile names — and a filename is allowed to contain a semicolon or a backtick. Joined into a string unquoted, that stops being an argument and becomes a command the next time the window is reopened.

So every argument is quoted the moment it is read out of /proc and stays quoted; nothing here builds a command by joining raw argv, and the reverse step only ever evaluates what this plugin quoted itself. scripts/test-capture tries six ways in and checks each one comes back as text.

Found in review by @ryanrhughes on the marketplace submission, and fixed before the plugin was listed.

Stored window data stays data

Window titles can be controlled by a page, and Qt's default Text.AutoText heuristic can treat a title as markup and load inline resources. Every direct label in the panel is therefore Text.PlainText; titles, commands and paths are shown literally even when they look like HTML.

The history also contains commands, directories, titles and recovered browser URLs. scripts/prepare-state opens the pathname once with O_NOFOLLOW, checks ownership and regular-file status on that descriptor, repairs its mode to owner-only (0600), and reads at most 1 MiB from the same descriptor. QML only accepts output carrying the helper's completion marker and never reopens the path through FileView for reading. Accepted history is capped at 40 rows with bounded fields, and serialized history has its own total ceiling.

What it cannot bring back

An editor's file. It returns to whatever it opens by default. Claude Code is the exception, and only because it can be told to continue the conversation for a directory.

A window whose command cannot be run again. Some apps are started by a launcher that does not survive in /proc, and single-instance apps hand off to an instance that may not open a window at all.

Development

omarchy plugin validate .
qmllint -I "$OMARCHY_PATH/shell" Service.qml BarWidget.qml Panel.qml
scripts/test-model      # addresses, the list, reopen rules, labels
scripts/test-wiring     # every root.x() the QML calls exists
scripts/test-capture    # no argument of a window's can become a command
scripts/test-hardening  # plain text, single-open private state, bounded input
scripts/capture-window <address>   # what one live window would be remembered as

Saving any file under ~/.config/omarchy/plugins/ hot-reloads the plugin; omarchy-restart-shell is what actually picks up changes to the service.

Remove

omarchy plugin remove io.github.monswiklund.recently-closed

License

MIT