~/research/2026-08-24

Hardening Omarchy's plugin installer against git transport-helper URLs

tl;dr

Omarchy's omarchy-plugin-add command clones a user-supplied git URL. It validated the plugin after cloning, but not the URL before cloning — so the only thing standing between a hostile URL like ext::sh -c <cmd> and command execution at clone time was a git configuration default the project doesn't control. The sibling command omarchy-theme-install already guarded against exactly this; my patch ports that guard to plugin-add and adds a regression test that proves both the rejections and that every legitimate URL shape still clones. No live RCE — this is defense-in-depth, and it's now upstream.

background: what plugin-add does

Omarchy is DHH's opinionated Arch-based Linux distribution, developed on GitHub under the basecamp organization. Its plugin system lets users extend the desktop by installing plugins straight from a git repository: omarchy-plugin-add <url> takes a URL (from the command line or from an interactive gum input prompt), runs git clone, and then validates and enables the plugin.

That ordering is the interesting part. All of the project's plugin validation happens after the clone. Whatever git clone itself does with the URL happens first — with no checks in front of it.

the attack surface: git transport helpers

A git URL is not just an address. Git supports remote transport helpers: a URL of the form <helper>::<address> tells git to hand the connection to an external helper program. Two forms matter here:

There is a second, related class: URLs that start with a dash. An argument like --upload-pack=<cmd> isn't a URL at all — if it reaches a git clone invocation in argument position, git parses it as an option, and --upload-pack in particular names a command to run. This is the classic argument-injection shape that has bitten many tools that pass user input to CLIs.

why this was not a live RCE

Git gates transport helpers behind protocol.ext.allow, and on a stock Omarchy system that default holds — there is no protocol.* override anywhere in the distribution, so an ext:: clone attempt dies before the helper runs. I verified that before writing a single line: no default-config exploit, no vulnerability report, no CVE theater.

The real finding is subtler: plugin-add's safety rested entirely on a git default the project doesn't set, doesn't test, and doesn't control. Any user tweak, distro change, or future git behavior shift silently becomes a remote-code-execution path — in a command whose whole job is fetching code from untrusted URLs.

Two more things sharpened the case. First, upstream had already accepted this exact reasoning once: PR #7884 ("Stop an installed theme from running code") added precisely this guard to omarchy-theme-install — but not to plugin-add, leaving the two sibling commands inconsistent. Second, a proposal (#7622) would let plugin URLs arrive from web pages, moving the input source further into untrusted territory.

the guard

The fix is deliberately small: apply the same guard theme-install uses, after both input paths (argv and the interactive prompt) have produced the final $url, and before git clone runs:

# git reads a leading dash as an option, and `<helper>::<address>` as a remote
# helper to run at clone time. Reject both so an untrusted URL cannot smuggle a
# transport helper that executes before the plugin is validated or enabled. An
# scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git carries `::` too and
# must still clone.
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
  fail "'$url' names a git option or transport helper, not a repository."
fi

The regex is the part worth reading twice. It rejects a string that starts with a scheme-shaped name ([A-Za-z0-9][A-Za-z0-9+.-]*) followed by :: — which is exactly the transport-helper grammar. It deliberately does not reject every URL containing ::, because legitimate URLs can carry one: an scp-style IPv6 address like git@[2001:db8::1]:org/repo.git has :: inside the brackets — but the characters before it (@, [) can't appear in a helper name, so the anchor at the start of the string lets it through. Naive "contains ::" filters break IPv6 users; this one doesn't.

What still clones, verified by test: https://…, scp-style git@host:org/repo.git, ssh://…, and the IPv6 form above. (Token-auth HTTPS URLs pass the guard too — same https:// shape — but they're not among the test's fixtures.) What gets refused: ext::…, fd::…, and anything shaped like an option.

proving it: the regression test

A guard like this is cheap to write and easy to break later, so most of the patch is test. The suite runs with no network and no real git: a stubbed git drops a marker file when its clone subcommand is reached. That turns "did the guard fire?" into a crisp, observable fact — if the marker exists, the URL got past the guard; if not, it was rejected first.

takeaways

Full patch and discussion: basecamp/omarchy #8067 — merged 2026-08-25.

< cd ~/basti.net