One tile, one menu
Click the tile to pause or resume recording; its subtitle reads “Recording” or “Paused”. The menu holds Current (with a Transform submenu), Pinned, Recent, Clear history and Preferences.
GNOME Shell extension
A private clipboard history in GNOME Quick Settings.
Keeps your last copies — text and images — in memory only, skips copies that KeePassXC and other password managers mark as secret, and turns JSON, base64, URLs and timestamps into what you need. Super+Shift+V opens it from the keyboard.
01
QuickClip adds one tile to Quick Settings and one popup under the keyboard. Both show the same history: your recent copies, kept in the Shell's memory and never written to disk except for snippets you explicitly pin.
Click the tile to pause or resume recording; its subtitle reads “Recording” or “Paused”. The menu holds Current (with a Transform submenu), Pinned, Recent, Clear history and Preferences.
Plain text and PNG screenshots are recorded. File managers usually offer copied files as plain text too, so a copied file is recorded as the text of its path. A rich format with no plain-text or PNG fallback is left on the clipboard, untouched.
A skipped copy leaves a row in its place — Sensitive copy skipped, Copy in an ignored app skipped, or Too large to keep — so protection stays visible without ever showing what was blocked.
Nothing you copy reaches disk, a log or a notification. The one exception is text you pin, stored in GSettings — and only because you asked.
02
x-kde-passwordManagerHint on what it
copies, for automatic skipping. KeePassXC and other
KDE-convention managers do; without one, use
Ignored apps in Preferences
instead.
$ curl -LO https://github.com/Ghost-Assembly/quickclip/releases/latest/download/[email protected] $ gnome-extensions install --force [email protected] $ gnome-extensions enable [email protected]
$ just setup $ just install $ just enable
Log out and back in. A newly installed extension is only picked up when the Shell next starts. On Wayland that means a new session.
03
Everything QuickClip keeps lives in the Shell's memory for as long as the session runs. The one exception is a snippet you pin — the only clipboard content ever written to disk, in GSettings (dconf), and only because you asked.
x-kde-passwordManagerHint MIME
type — set by KeePassXC and other apps that follow the
KDE convention — is never read, so its content never
reaches QuickClip's memory. A manager that does not set
this hint, such as some Electron-based ones, is not
detected this way; add it to
Ignored apps in Preferences
instead.
QuickClip declares the unlock-dialog session mode
so its history can survive a screen lock — but only when
Clear on lock is turned off. Either way, while the
screen is locked there is no tile, no popup, no keybinding and
nothing is recorded. With Clear on lock on (the
default), the history empties the moment the screen locks; with
it off, the history stays in memory across the lock and is there
again on unlock.
Never logged, never notified. Log lines carry only the kind of copy or the reason it was skipped — never its content — and no notification ever quotes what you copied. Only a pin persists, and only in GSettings.
Freeing memory. Dropping an item, clearing the history, or disabling the extension only drops QuickClip's own reference to the text. JavaScript engines are free to keep that string's memory until garbage collection runs, so a very sensitive copy may stay reachable in memory slightly longer than the history shows it.
04
Every transform is offered from Current's Transform submenu, or with Tab in the popup, whenever the copy on top qualifies. Two need no input at all and are always offered: they generate a fresh value instead of changing the copy.
| Transform | Example | Offered when |
|---|---|---|
| Pretty-print JSON |
{"a":1} → the same, indented two spaces
per level
|
The copy parses as JSON |
| Minify JSON |
{ "a": 1 } → {"a":1}
|
The copy parses as JSON |
| Base64 encode |
hello → aGVsbG8=
|
Any non-empty copy |
| Base64 decode |
aGVsbG8= → hello
|
The copy looks like base64 and decodes to text |
| URL encode |
a b/c → a%20b%2Fc
|
Encoding would change the copy |
| URL decode |
a%20b → a b
|
The copy contains %XX escapes
|
| Trim whitespace |
hi →
hi
|
Leading or trailing whitespace |
| Collapse whitespace |
a b → a b
|
Runs of whitespace, tabs or newlines |
| UPPER CASE |
Hi → HI
|
The copy has lower-case letters |
| lower case |
Hi → hi
|
The copy has upper-case letters |
| snake_case |
helloWorld → hello_world
|
A single-line identifier under 200 characters |
| kebab-case |
helloWorld → hello-world
|
A single-line identifier under 200 characters |
| Unix time → ISO date |
1700000000 →
2023-11-14T22:13:20.000Z
|
A 10- or 13-digit Unix timestamp |
| ISO date → Unix time |
2023-11-14T22:13:20Z →
1700000000
|
An ISO date or date-time |
| New UUID |
generates 0b2b6b0e-…-…
|
Always — a generator, ignores the copy |
| Current time (ISO) |
generates the current time, e.g.
2026-09-26T18:02:00.000Z
|
Always — a generator, ignores the copy |
A transform works only on a copy of up to 100,000 characters; a longer copy is offered only the generators, which ignore it. A date-time with no time zone is read as local time, the same as your system clock, not UTC.
05
Open them from the Extensions app, or with
just prefs from a clone. Recording is paused and
resumed from the tile or the pause shortcut, not from this
window; the state is remembered between sessions.
| Setting | Default | Notes |
|---|---|---|
| History size | 20 | 5 to 100. Oldest copies are dropped first. |
| Image memory (MB) | 32 | 0 to 256. Memory copied images may use together; 0 keeps no images. |
| Expire after (minutes) | 30 | 0 to 1440. 0 keeps a copy until it is pushed out by History size. |
| Clear on lock | On | Empty the history when the screen locks. |
| Paste on select | On | Paste the chosen item into the window that had focus. |
| Open the popup | Super+Shift+V | A conflicting shortcut is refused, never overwritten. |
| Pause or resume | Not set | Same conflict check as the popup shortcut. |
| Ignored apps | None | Copies made while one of these is focused are skipped. |
| Terminal apps | Ptyxis, GNOME Console, GNOME Terminal, kitty, Alacritty, foot, WezTerm, Ghostty | These paste with Ctrl+Shift+V instead of Ctrl+V. |
| Pinned snippets | None | The only clipboard content stored on disk; kept until you unpin it. |
Setting a shortcut is checked against GNOME's own bindings (the window manager, the Shell, mutter, media keys and any custom shortcut) and QuickClip's other shortcut. A conflicting choice is refused, with a toast naming what already uses it — never silently overwritten. Other extensions' shortcuts are not visible to this check, so a conflict with one of those is not caught here.
06
Super+Shift+V opens the popup over any application window. It does nothing in the Activities overview or while the screen is locked.
| Input | Does |
|---|---|
| Super+Shift+V | Open the popup |
| Typing | Filter the list by matching text |
| ↑ / ↓ | Move the selection, wrapping at both ends |
| Enter | Copy the selection and, after a short delay, paste it into the window that had focus |
| Tab | Switch to the Transform list for the selection; Tab again returns |
| Esc | Leave the Transform list, or close the popup — twice from Transform view |
Auto-paste sends Ctrl+Shift+V to the apps listed in Terminal apps — Ctrl+V would just insert a literal “v” there — and Ctrl+V to everything else, after a short delay so the previous window has focus back first.
Clicking an item in the Quick Settings tile's menu copies it to the clipboard but never pastes it — pasting only happens from the keyboard popup, and only when Paste on select is on. Super+V is untouched: it still opens GNOME's own notification list.
07
Every decision — what to record, what a transform does, what a shortcut may take — lives in a module that imports no GNOME API, only other modules like it, so the suite can test it whole on plain Node.
| Module | Job | Deps |
|---|---|---|
extension.js
|
The entry point, deliberately thin: builds
ClipboardSource,
Paster and QuickClip on
enable and tears them down on disable. Nothing runs
at import time.
|
GLib, Shell extensions base |
modules/model.js
|
History: add/dedupe, size and image-budget caps,
expire(), nextExpiry(),
clear, change callbacks
|
none (pure) |
modules/transforms.js
|
The
{id, label, applies(text), run(text)}
list; uuid and clock injected
|
model.js (pure) |
modules/privacy.js
|
shouldRecord({mimetypes, appId, paused,
ignoredApps, imagesAllowed})
→ {record, kind} or
{record, reason}
|
model.js (pure) |
modules/listing.js
|
Row text and previews, the popup filter, which text can be pinned | model.js, privacy.js, transforms.js (pure) |
modules/accel.js
|
Accelerators compared as GNOME matches them; conflicts; Shift-only refusal | none (pure) |
modules/settings.js
|
The key list and its wording; a watcher that releases what it connects | none (pure) |
modules/recorder.js
|
Clipboard change → privacy check → read → History; drops superseded reads; the one expiry timer | model.js, privacy.js, settings.js (no GNOME API) |
modules/clipboard.js
|
Meta.Selection
owner-changed (CLIPBOARD) → read via
St.Clipboard; write text/image
|
GLib, Meta, Shell, St |
modules/paste.js
|
Virtual keyboard device; Ctrl+V / Ctrl+Shift+V by focused app id | Clutter, GLib |
modules/controller.js
|
Builds the pieces; lock handling, keybindings and every action a click or key asks for | Meta, Shell, Shell UI |
modules/panel.js
|
QuickMenuToggle: subtitle
Recording/Paused; menu Current (+transform row),
Pinned, Recent, Clear history, Preferences
|
Shell UI |
modules/popup.js
|
Modal list: Pinned then Recent; type-to-filter text; ↑/↓, Enter = copy+paste, Tab = transform row, Esc | Shell UI |
prefs.js
|
Adw preferences window, in its own process | Adw, Gtk, Gdk, Gio |
Two files are excluded from coverage, both
toolkit plumbing a unit test could only assert against a
stub of the toolkit: prefs.js (Adw and Gtk
widget construction — the key list, wording and shortcut
rules it uses live in modules/settings.js and
modules/accel.js, tested directly) and
modules/clipboard.js (St.Clipboard
and Meta.Selection calls, checked against a
real Shell by scripts/headless-check.sh
instead). See the
security policy.
08
The unit suite runs on Node with the Shell's modules replaced by
recording stubs. just test-live adds what CI cannot
have: it boots a headless GNOME Shell, enables QuickClip, and
checks that it starts listening and survives a disable and
re-enable with no JavaScript errors or leaked signal or timeout
sources. When a virtual clipboard can own the selection in that
headless session it also checks that a plain-text copy is
recorded and a password-manager copy is skipped; a headless
Shell has no real seat, so those two checks are sometimes
skipped there and covered by the manual checklist below instead.
Run through this in a real session before a release; nothing here can run in CI, which has no GNOME Shell.
{"a":1}; Enter on
Pretty-print JSON pastes the pretty result.
%E0%A4%A and choose URL decode → a
notification reads “URL decode did not apply — Not valid
URL encoding”, quoting nothing from the clipboard, and
the clipboard is unchanged. No notification ever quotes
clipboard text.
toggle-message-tray.
just logs shows only
[quickclip] lines with kinds and reasons —
no clipboard content.
09
just build makes the zip with plain
zip, because gnome-extensions ships
inside the gnome-shell package and CI has no GNOME.
scripts/pack-check.sh keeps the official tool as
the authority, diffs the two listings and confirms every shipped
icon decodes — an icon that fails to load is silent.
The zip holds metadata.json,
extension.js, prefs.js,
modules/, schemas/,
icons/ and stylesheet.css, which
styles the tile and menu beyond what the Shell already styles.
The uuid is written down once, in metadata.json,
and everything else reads it from there.
10
Set the version in metadata.json and
package.json, commit, tag and push. The release
workflow checks the tag against both files before building.
$ git tag -a v0.1.0 -m 'release v0.1.0' $ git push origin v0.1.0
11
just # list every recipe just test # unit suite just test-docs # this site, in Chromium and Firefox just lint # eslint, prettier, gschema, shellcheck just ci # what CI runs: lint, tests, test-docs, security, build just test-live # headless gnome-shell smoke test, then the packed zip just coverage # unit suite with a coverage report just docs # serve this site on localhost:8000
Every decision lives in a module that imports no GNOME API, so the unit suite tests it on plain Node; see Architecture above.