Secret Canary
The screen floods red the instant an AWS key, GitHub token, or private key escapes into your clipboard or a staged git diff. One key redacts it.
This is an Omarchy shell plugin (service + overlay + bar-widget). It runs
inside the long-lived omarchy-shell process. It does not start a second
Quickshell instance. Detection is offline: no accounts, no network.
Install
omarchy plugin add https://github.com/ccdwyer/omarchy-secret-canary.git --enable
Then install canaryd (recommended). QML falls back to compat/canaryd.sh
if bin/canaryd is missing or is not a compiled helper — that path is
poll-only and does not do surgical git redaction (see limitations).
build.sh only writes bin/canaryd after a successful cargo release
build; a missing toolchain does not copy the shell fallback into
bin/ (that would make the plugin think the surgical helper is present).
Source (any machine with Rust):
~/.config/omarchy/plugins/io.github.chris.secret-canary/build.sh
Linux artifact (after a git tag): GitHub Actions
(.github/workflows/release.yml) smoke-tests and publishes
canaryd-x86_64-unknown-linux-musl and
canaryd-aarch64-unknown-linux-musl plus SHA256SUMS-*. This tree does
not contain those binaries (it was authored on macOS). Download from the
Release, verify the checksum, and copy onto bin/canaryd:
install -m 0755 canaryd-x86_64-unknown-linux-musl \
~/.config/omarchy/plugins/io.github.chris.secret-canary/bin/canaryd
Put the chip on the bar if --enable did not:
omarchy bar put io.github.chris.secret-canary --section right
Reload plugins if the shell was already running:
omarchy-shell shell rescanPlugins
60-second demo
- Left-click the canary chip → Test the canary. The overlay floods.
- Press Enter. Paste somewhere:
[REDACTED by Secret Canary]. - Optional git path — from this plugin:
cd ~/.config/omarchy/plugins/io.github.chris.secret-canary/demo
git init
# add that directory as a watched repo in the popup, then:
git add .env
With the Rust helper (bin/canaryd), Enter reverse-applies only the
secret hunk so KEEP_ME stays staged. A cold install that is still on
compat/canaryd.sh unstages the whole incident file and labels it
file unstaged (all hunks). On a fresh git init with no HEAD that
unstage is git rm --cached -- <file> (worktree kept); after the first
commit it is git restore --staged. Neither path touches the working tree.
Copy a random UUID. Nothing happens.
What it watches
| Path | How | Alarm |
|---|---|---|
| Regular clipboard | wl-paste -w child, plus a startup wl-paste -n. If the watcher cannot spawn, 500 ms wl-paste -n poll (clipboard: poll) |
Tier-1 overlay, tier-2 amber chip |
| Staged diffs | Explicit git roots only; git rev-parse --git-path index |
Tier-1 overlay. Tier-2 is log-only |
Primary selection (middle-click paste) is not watched. Terminal scrollback is not read. The public claim is clipboard + staged diffs.
Git roots are an explicit add in the bar popup. Nothing under ~/Developer
is scanned unless you say so.
Overlay keys
The alarm is a real overlay with exclusive keyboard focus.
| Key | Action |
|---|---|
| Enter | Redact clipboard, or surgically unstage the secret hunk |
| R | Restore the pre-alarm clipboard (held in memory only) |
| A | Allowlist this value (SHA-256 stored, never plaintext) |
| Esc | Dismiss (30 s auto-dismiss) |
Super+Ctrl+X is Omarchy dictation and Super+Ctrl+C is the capture menu, so
Canary prefers Super+Alt+X (redact) and Super+Alt+Shift+C (summon). Hotkeys
are opt-in from the bar: if none is installed, the chip shows Set
hotkey. That writes a marked o.bind block to
~/.config/hypr/bindings.lua (Hyprland reloads on save). Occupied shortcuts
are skipped; Super+Alt+X falls back to Super+Shift+Alt+X, and summon falls
back to Super+Alt+C only if that combo is free (Chroma may already own it).
The plugin never auto-assigns on first load and never hl.unbinds someone
else's key. When a hotkey is set, the bar and settings show it; Change
hotkey rewrites this plugin's block, Remove hotkey deletes only that
block.
bind = SUPER ALT, X, exec, omarchy-shell io.github.chris.secret-canary redact ''
bind = SUPER ALT SHIFT, C, exec, omarchy-shell shell summon io.github.chris.secret-canary '{}'
omarchy-shell io.github.chris.secret-canary installBinds ''
omarchy-shell io.github.chris.secret-canary installBinds change
omarchy-shell io.github.chris.secret-canary removeBinds ''
Left-click the chip for settings (Test the canary, mute, watched repos, hotkey). Right-click tests immediately. A red chip opens the alarm overlay.
Tiers
Tier 1 (overlay): AKIA… keys, GitHub ghp_ / gho_ / ghs_ /
github_pat_, PEM private-key headers, sk- API keys, Slack xox[baprs]-.
Tier 2 (amber chip only, and log-only in git): JWTs, and high-entropy
tokens (Shannon > 4.2 over 24+ chars) sitting near
secret|token|password|api_key.
Honest limitations
- Detection + fast remediation, not prevention. Between the copy and Enter, another app can still read the clipboard. The README will not pretend otherwise.
- Clipboard managers. Canary cannot delete entries from a third-party history. If a manager re-injects the secret within 60 s of a redact, you get a follow-up warning ("your clipboard manager restored it").
- Git redaction is surgical first, honest always — in
canaryd. The Rust helper reverse-applies only secret added lines withgit apply --cached -R. If that fails, the file is unstaged wholesale and the UI says file unstaged (all hunks). The worktree is never modified.compat/canaryd.shcannot do the surgical path; it unstages the incident file wholesale (git restore --staged, orgit rm --cachedon a freshgit initwith noHEAD) and labels itfile unstaged (all hunks). - No terminal scrollback. Anything copied from a terminal transits the clipboard watcher; anything staged transits the git watcher.
- Regular clipboard only. Primary selection is a v1.1 gap.
- Helper binary.
bin/canarydis built bybuild.sh(no prebuilt Linux musl binaries in this tree). If it is missing, QML runscompat/canaryd.sh: clipboard polled at 500 ms, git polled every 5 s, tier-1 grep only (no JWT, no entropy), no inotify. Allowlist hashes and watched-repo lists still persist (allow.jsonmode 0600,watch.json, 64-repo cap, targeted unwatch). After three daemon deaths the chip goes amber degraded — it will not silently claim it is watching. - Sound is off. The visual flood is the alarm. Chime is an opt-in on the
chip (and the
soundbar-widget setting). - Keybinds are opt-in from the bar. Overlay keys work while the alarm is up. Occupied combos are skipped. Never
hl.unbind. No first-load auto-assign.
Threat model (the watcher)
canaryd sees clipboard bytes. It is engineered as the least-trusted
component:
- Content is scanned in a streaming buffer and dropped. No clipboard content is ever persisted or logged.
- Events carry a preview truncated to the first 4 characters (
AKIA…) and nothing else. - The allowlist stores SHA-256 of values, never plaintext
(
~/.config/secret-canary/allow.json, mode 0600). - Raw matches never appear in logs or argv.
- The redaction string's hash is permanently suppressed, so overwriting the clipboard never re-triggers the scanner.
Settings
Bar-widget settings arrive inline on the shell.json layout entry. There is
no plugin settings file for those.
| Key | Default | Meaning |
|---|---|---|
sound |
false |
Play a theme-ish chime on tier-1 alarm |
hideUntilEvent |
false |
Hide the chip until the first event |
Watched git roots are operational state (~/.local/state/secret-canary/watch.json),
not widget settings. Allowlisted hashes and disabled rule ids live in
~/.config/secret-canary/allow.json (mode 0600). Settings can toggle each
rule on/off via allow-rule / enable-rule.
Mute 1 hour is a runtime action on the settings overlay.
IPC
shell summon / hide show the overlay. Service verbs (redact, status, …)
live on the keep-loaded service IpcHandler. omarchy-shell shell call <id>
hits the overlay loader only (open/close/toggle), so it cannot redact. Always
pass the string argument, even when unused:
omarchy-shell io.github.chris.secret-canary redact ''
omarchy-shell io.github.chris.secret-canary allowRule jwt
omarchy-shell io.github.chris.secret-canary enableRule jwt
omarchy-shell io.github.chris.secret-canary restore ''
omarchy-shell io.github.chris.secret-canary test ''
omarchy-shell io.github.chris.secret-canary mute ''
omarchy-shell io.github.chris.secret-canary status ''
omarchy-shell shell summon io.github.chris.secret-canary '{}'
omarchy-shell shell hide io.github.chris.secret-canary
Tests (off-device)
node tests/run.js
bash tests/fallback.test.sh
# helper, if you have cargo:
cargo test --manifest-path src/canaryd/Cargo.toml
Remove
omarchy plugin remove io.github.chris.secret-canary
If you used Set hotkey, also delete this plugin's marked block from
~/.config/hypr/bindings.lua (Hyprland reloads on save):
-- BEGIN io.github.chris.secret-canary
o.bind("SUPER + ALT + X", "Secret Canary redact", "omarchy-shell io.github.chris.secret-canary redact ''")
o.bind("SUPER + ALT + SHIFT + C", "Secret Canary", "omarchy-shell shell summon io.github.chris.secret-canary '{}'")
-- END io.github.chris.secret-canary
Or click Remove hotkey on the bar / in settings before uninstalling.
That script only strips this plugin's BEGIN/END block — it never
hl.unbinds someone else's shortcut.