Files
omarchy/bin/omarchy-plugin-add
T
OmarchybotandClaude Opus 5 e713ff3166 Share the git URL check, and refuse the transports Omarchy does not clone from (backport of #8174)
Backport of the shared URL check (PR #8174, merged to quattro as 68ab12f7) onto
the v4-0-1 release branch, on top of the #8067 backport it follows.

omarchy-theme-install and omarchy-plugin-add both clone a URL a stranger can
choose, and each carried its own copy of the rule that refuses a git option or a
transport helper before cloning. The rule now lives in omarchy-git-url-check and
both callers ask it: two copies of a security check drift, and the second copy
arrived four months after the first only because someone went looking for it.

That rule was also enforcing half of what it described. <helper>::<address> is
one of two shapes git resolves a remote helper from -- it also runs
git-remote-<scheme> for <scheme>://<address> whenever the scheme is not one it
connects itself, so ext::sh -c id and ext://sh -c id reach the same helper while
only the first was refused. Nothing exploitable follows on a stock system:
protocol.ext.allow defaults to never, and the :// spelling hands git-remote-ext
a command name it cannot exec. But a third-party helper installed on PATH is
reachable through the scheme form alone, and a guard is worth more when it
enforces the rule it states.

The :// shape cannot be refused the way :: is, because it is also how every
legitimate URL arrives, so the scheme is checked against the transports git
still connects itself: ssh git git+ssh ssh+git http https ftp ftps file.
git+ssh and ssh+git are on that list because they are spelled like a helper and
read as plain ssh; leaving them off would refuse a URL that clones today. ext
and fd are off it deliberately. A single colon is always scp-style ssh and a
bare path is always a path, so neither needs constraining.

The check fails closed: the callers read a non-zero status as a refusal, so a
missing omarchy-git-url-check refuses the URL rather than waving it through.

Clean cherry-pick on top of the #8067 backport: every file is byte-identical to
quattro, so merging v4-0-1 into quattro resolves without a conflict. test/shell
passes: 189 files, including the one this adds. test/cli passes, so the new
command's metadata is well-formed. Exercised the check here: https, scp-style,
ssh, git+ssh and scp-style IPv6 all pass, while ext:: ext:// fd:: fd:// and a
leading-dash form are refused, as is an uppercase EXT:// spelling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 09:13:35 +02:00

176 lines
4.2 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Add a shell plugin from git
# omarchy:group=plugin
# omarchy:args=[git-url] [--enable] [--yes]
# omarchy:examples=omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
# omarchy:alias=omarchy plugin install
set -euo pipefail
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -oBatchMode=yes}"
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
ASSUME_YES=0
fail() {
echo "omarchy-plugin-add: $*" >&2
exit 1
}
interactive() {
[[ -t 0 && -t 1 ]]
}
confirm() {
local prompt="$1"
(( ASSUME_YES )) && return 0
if interactive; then
gum confirm "$prompt"
else
fail "refusing to continue without confirmation; pass --yes"
fi
}
ENABLE_PLACEMENT=()
select_bar_widget_placement() {
local id="$1"
local section
local default_section
interactive || return 0
(( ASSUME_YES )) && return 0
jq -e '(.kinds // []) | (index("bar") | not) and (index("bar-widget") != null)' \
"$PLUGINS_DIR/$id/manifest.json" >/dev/null 2>&1 || return 0
default_section=$(jq -r '.barWidget.defaultSection // "center"' "$PLUGINS_DIR/$id/manifest.json")
section=$(printf '%s\n' left center right |
gum choose --header="Place $id in which bar section?" --selected "$default_section") || return 0
[[ -n $section ]] || return 0
ENABLE_PLACEMENT=(--section "$section")
}
plugin_id_manifest() {
omarchy-plugin-catalog | jq -r --arg id "$1" '
map(select(.id == $id))[0].manifestPath // empty
'
}
url=""
enable_after=""
while (( $# > 0 )); do
case "$1" in
--enable)
enable_after=true
shift
;;
--yes | -y)
ASSUME_YES=1
shift
;;
-h | --help)
echo "Usage: omarchy plugin add [git-url] [--enable] [--yes]"
exit 0
;;
-*)
fail "unknown add option: $1"
;;
*)
[[ -z $url ]] || fail "unexpected argument: $1"
url="$1"
shift
;;
esac
done
if [[ -z $url ]]; then
interactive ||
fail "a git URL is required (e.g. omarchy plugin add https://github.com/acme/omarchy-weather.git)"
url=$(gum input --prompt "Git URL of the plugin repo: ") || fail "cancelled"
[[ -n $url ]] || fail "a git URL is required"
fi
# Refuse a URL that names a git option or a transport helper before cloning, so
# an untrusted URL cannot run a command before the plugin is validated or
# enabled. The check is shared with omarchy-theme-install and explains itself; a
# missing checker leaves this non-zero, which refuses the URL rather than
# cloning it.
omarchy-git-url-check "$url" || exit 1
if (( ! ASSUME_YES )); then
cat >&2 <<WARN
⚠️ Plugins run as arbitrary, unsandboxed code inside your long-lived
omarchy-shell process. Only add repos you trust, and review the code
before you enable it.
URL: $url
WARN
confirm "Clone and add this plugin?" || fail "aborted"
fi
mkdir -p "$PLUGINS_DIR"
stage="$PLUGINS_DIR/.add.tmp.$$"
rm -rf "$stage"
if ! git clone -- "$url" "$stage"; then
rm -rf "$stage"
fail "failed to clone $url"
fi
if ! omarchy-plugin-validate "$stage"; then
rm -rf "$stage"
fail "refusing to add: validation failed"
fi
id=$(jq -r '.id' "$stage/manifest.json")
existing_manifest=$(plugin_id_manifest "$id") || {
rm -rf "$stage"
fail "could not inspect installed plugin ids"
}
if [[ -n $existing_manifest ]]; then
rm -rf "$stage"
fail "plugin id '$id' is already used by $existing_manifest"
fi
target="$PLUGINS_DIR/$id"
if [[ -e $target || -L $target ]]; then
rm -rf "$stage"
fail "plugin '$id' is already installed; update it with: omarchy plugin update $id"
fi
mv "$stage" "$target"
echo "Added $id into $target"
omarchy-shell shell rescanPlugins >/dev/null
if [[ -z $enable_after ]]; then
if (( ASSUME_YES )) || ! interactive; then
enable_after=false
elif confirm "Enable '$id' now?"; then
enable_after=true
else
enable_after=false
fi
fi
if [[ $enable_after == true ]]; then
select_bar_widget_placement "$id"
discovered=0
for (( attempt = 0; attempt < 40; attempt++ )); do
if omarchy-plugin-list --json | jq -e --arg id "$id" 'any(.[]; .id == $id)' >/dev/null; then
discovered=1
break
fi
sleep 0.05
done
(( discovered )) || fail "plugin '$id' is not known"
omarchy-plugin-enable "$id" "${ENABLE_PLACEMENT[@]}"
else
echo "Enable it later with: omarchy plugin enable $id"
fi