Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
68d8141
OM-SEC-12: Bind update inhibitor cleanup to process identity
AFOliveira Aug 31, 2026
1136a71
OM-SEC-14: Run update hooks without reusable sudo authority
AFOliveira Aug 31, 2026
87625c2
OM-SEC-01: Make passwordless sudo expiry fail closed
AFOliveira Aug 31, 2026
2f816a8
Document privileged Bash startup exception
AFOliveira Sep 1, 2026
7f9a401
Merge commit 'b71dcad96e9d0b2962b7d225828a5cb6000ad720' into codex/re…
AFOliveira Sep 6, 2026
35b318e
Complete command-scoped authentication across update phases
AFOliveira Sep 6, 2026
c52381f
Keep update regressions isolated from host authentication
AFOliveira Sep 6, 2026
f2c3925
Revoke before session cleanup and protect standalone inhibition
AFOliveira Sep 6, 2026
1f8f5ba
Integrate current quattro before security review
AFOliveira Sep 6, 2026
a9e9954
Clarify unattended updates still require sudo authorization
AFOliveira Sep 6, 2026
978dffa
Use required gum directly in AI removal prompts
AFOliveira Sep 6, 2026
d41ca88
Wait for the actual Stay Awake launcher
AFOliveira Sep 6, 2026
177d572
Wait for notifications when restarting the shell
AFOliveira Sep 6, 2026
35551b0
Disable the Yay sudo loop with its supported option
AFOliveira Sep 6, 2026
aa422fc
Merge current Quattro while preserving command checks
AFOliveira Sep 7, 2026
5b692c5
Preserve user PATH across updater relaunches
AFOliveira Sep 7, 2026
f37c73f
Bind protected update commands to their source root
AFOliveira Sep 7, 2026
5bc41b2
Resolve security libraries beside canonical entrypoints
AFOliveira Sep 7, 2026
c869740
Keep channel transitions inside command-scoped sudo
AFOliveira Sep 7, 2026
0add43f
Check dev update support before changing the system
AFOliveira Sep 7, 2026
bc235d2
Merge remote-tracking branch 'refs/remotes/portfolio/quattro' into co…
AFOliveira Sep 7, 2026
4ae25cd
Keep temporary sudo grants bounded through lifecycle failures
AFOliveira Sep 7, 2026
a6385e6
Bound sudo policy natively and guard expiry package transactions
AFOliveira Sep 7, 2026
75e58f0
Integrate the shared sudo lifecycle foundation
AFOliveira Sep 7, 2026
104c776
Merge update inhibitor identity hardening
AFOliveira Sep 7, 2026
76f0abe
Roll back interrupted inhibitor launches
AFOliveira Sep 7, 2026
0240113
Open launch authorization without recreating cancelled state
AFOliveira Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Three documentation trees, split by genre and audience:
- Prefer `(( ))` over numeric operators inside `[[ ]]` (e.g., `(( count < 50 ))`, not `[[ $count -lt 50 ]]`)
- Prefer a full `if`/`else` conditional for simple two-path control flow; don't rely on `exec` or `exit` in one branch to make following statements unreachable
- For strings/paths with spaces, quote them instead of escaping spaces with `\ ` (e.g., `"$APP_DIR/Disk Usage.desktop"`, not `$APP_DIR/Disk\ Usage.desktop`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`). A security-sensitive entrypoint may use the exact `#!/bin/bash -p` form only when it must suppress `BASH_ENV` and exported-function startup injection before its first command; that exception must be explained at the boundary and covered by a regression that rejects an ordinary Bash launch with a decoy `-p` argument.
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs

# Command Naming
Expand Down
38 changes: 33 additions & 5 deletions bin/omarchy-channel-set
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
#!/bin/bash
#!/bin/bash -p

# omarchy:summary=Set the Omarchy package channel.
# omarchy:args=<stable|rc|edge|dev>
# omarchy:requires-sudo=true

if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup for channel switching." >&2
exit 126
fi
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
set -euo pipefail
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
user_path=$PATH
omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_install_sudo_cleanup_traps
omarchy_security_enable_no_update_sudo

usage() { echo "Usage: omarchy-channel-set [stable|rc|edge|dev]"; }
fail() { echo "Error: $*" >&2; exit 1; }
Expand Down Expand Up @@ -33,9 +46,18 @@ validate_dev_checkout() {
}

link_dev_checkout() {
local checkout="$1"
local checkout="$1" required
[[ -d $checkout/.git ]] || git clone https://github.com/basecamp/omarchy.git "$checkout"

# Check the destination before changing /etc/omarchy.conf or sudo's path.
# An existing checkout is not pulled automatically and may predate this policy.
for required in bin/omarchy-security-functions bin/omarchy-update bin/omarchy-refresh-pacman default/omarchy/sudo-no-update/sudo; do
if [[ ! -f $checkout/$required || ! -r $checkout/$required ||
( $required != "bin/omarchy-security-functions" && ! -x $checkout/$required ) ]]; then
fail "Update the checkout before switching to dev; missing required update support in $required."
fi
done

omarchy-dev-link "$checkout" --no-reboot
}

Expand Down Expand Up @@ -79,21 +101,27 @@ fi
if [[ -n $dev_checkout ]]; then
link_dev_checkout "$dev_checkout"
export OMARCHY_PATH="$dev_checkout"
export PATH="$OMARCHY_PATH/bin:$PATH"
omarchy_security_enable_no_update_sudo
omarchy-state set reboot-required
fi

omarchy-refresh-pacman "$pacman_channel"
omarchy-refresh-pacman "$pacman_channel" defer-hook
# --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites.
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -S --needed --noconfirm --ask 4 "${packages[@]}"

if [[ -z $dev_checkout ]]; then
omarchy-dev-unlink --no-reboot
export OMARCHY_PATH=/usr/share/omarchy
omarchy_security_enable_no_update_sudo

if (( leaving_dev )); then
omarchy-state set reboot-required
fi
fi

omarchy-update -y
OMARCHY_UPDATE_USER_PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update" -y

# No channel-owned privileged work follows the historical refresh hook.
omarchy_security_revoke_sudo_timestamp
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" \
"$OMARCHY_PATH/bin/omarchy-refresh-pacman" "$pacman_channel" run-deferred
54 changes: 38 additions & 16 deletions bin/omarchy-refresh-pacman
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
#!/bin/bash
#!/bin/bash -p

# omarchy:summary=Overwrite the package configuration for /etc/pacman with the Omarchy default of using its dedicated mirrors and repositories, then update all packages.
# omarchy:requires-sudo=true

sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi

channel="${1:-stable}"
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
user_path=$PATH
omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_install_sudo_cleanup_traps
omarchy_security_enable_no_update_sudo

channel="${1:-stable}"
hook_mode="${2:-normal}"
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Error: Invalid channel '$channel'. Must be one of: stable, rc, edge"
exit 1
echo "Invalid channel: $channel" >&2
exit 2
fi
if [[ $hook_mode != "normal" && $hook_mode != "defer-hook" && $hook_mode != "run-deferred" ]]; then
echo "Invalid refresh hook mode: $hook_mode" >&2
exit 2
fi

echo "Setting channel to $channel"
echo

sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist

# Allow user customization of /etc/pacman.conf before the upgrade runs
omarchy-hook pre-refresh-pacman
if [[ $hook_mode != "run-deferred" ]]; then
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel"
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm
fi

# Reset all package DBs and then update
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm
# Keep the historical hook name, but finish every privileged refresh operation
# before running user code. Callers with later root work can defer the hook.
if [[ $hook_mode != "defer-hook" ]]; then
omarchy_security_revoke_sudo_timestamp
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" \
"$OMARCHY_PATH/bin/omarchy-hook" pre-refresh-pacman
fi
2 changes: 1 addition & 1 deletion bin/omarchy-remove-ai-hermes
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ fi
# never owned, a yes takes that with it, and saying so is the prompt's job.
# Without a terminal to ask in, keeping everything is the answer.
data_removed=false
if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]] && omarchy-cmd-present gum; then
if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]]; then
# du answers non-zero when either directory is missing, and pipefail would
# turn that into an aborted removal; the size is worth no such thing.
size=$(du -shc "$HOME/.hermes" "$HOME/.config/Hermes" 2>/dev/null | tail -1 | cut -f1 || true)
Expand Down
2 changes: 1 addition & 1 deletion bin/omarchy-remove-ai-openclaw
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
# front of the user with the size rather than left silent. Without a terminal
# to ask in, keeping it is the answer.
state_removed=false
if [[ -d $HOME/.openclaw && -t 0 ]] && omarchy-cmd-present gum; then
if [[ -d $HOME/.openclaw && -t 0 ]]; then
size=$(du -sh "$HOME/.openclaw" 2>/dev/null | cut -f1)
if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, and downloaded plugins)?"; then
rm -rf "$HOME/.openclaw"
Expand Down
16 changes: 15 additions & 1 deletion bin/omarchy-restart-shell
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ relock_session() {
return 1
}

notifications_ready() {
[[ $(busctl --user call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus NameHasOwner s org.freedesktop.Notifications 2>/dev/null) == "b true" ]]
}

# Core IPC can answer before the notification plugin has registered its bus
# name. Restore an existing notification service before update hooks or setup
# invitations send their one-time toasts; a disabled service need not appear.
notifications_were_running=0
if notifications_ready; then
notifications_were_running=1
fi

# Each kill stops the oldest matching instance and only returns once it has
# fully exited, so the no-duplicate launch below can't race a dying shell.
while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1; do :; done
Expand All @@ -70,7 +83,8 @@ while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1;
hyprctl dispatch 'hl.dsp.exec_cmd("omarchy-launch-shell")' >/dev/null

for (( attempt = 0; attempt < 20; attempt++ )); do
if OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1; then
if OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1 &&
{ (( notifications_were_running == 0 )) || notifications_ready; }; then
# The session stays compositor-locked after the old lock client died, so
# re-acquire the lock and let the user authenticate out of it.
if (( relock )) && ! relock_session; then
Expand Down
137 changes: 137 additions & 0 deletions bin/omarchy-security-functions
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/bin/bash

# omarchy:hidden=true
# omarchy:summary=Provide internal helpers for command-scoped sudo authentication

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
echo "omarchy-security-functions is an internal function library." >&2
exit 64
fi

omarchy_security_require_privileged_bash_startup() {
[[ $- == *p* ]] || return 1
/usr/bin/env -i /usr/bin/bash -p -c '
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
[[ $executable == "/usr/bin/bash" &&
( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) &&
${argv[1]:-} == "-p" ]]
' omarchy-bash-startup "$$"
}

omarchy_security_sanitize_bash_environment() {
local script=$1
shift
local entry name environment_fd environment_pid
local -a unsets=()

# Read the raw environment: privileged Bash ignores exported functions, but
# leaves their records for ordinary child interpreters to import later.
exec {environment_fd}< <(/usr/bin/env -0)
environment_pid=$!
while IFS= read -r -d '' entry <&"$environment_fd"; do
name=${entry%%=*}
case "$name" in
BASH_ENV|ENV|SHELLOPTS|BASHOPTS|PS4|CDPATH|GLOBIGNORE|BASH_FUNC_*%%)
unsets+=(-u "$name")
;;
esac
done
exec {environment_fd}<&-
wait "$environment_pid" || return 1
if (( ${#unsets[@]} > 0 )); then
exec /usr/bin/env "${unsets[@]}" /usr/bin/bash -p -- "$script" "$@"
fi
}

omarchy_security_require_source_root() {
local command_source command_name=${1##*/}
command_source=$(/usr/bin/readlink -e -- "$1") || return 1

# A runtime root selects the code used by this invocation. Accept the
# canonical checkout containing the entrypoint or the package's bin links.
if [[ ${OMARCHY_PATH:-} != /* || $(/usr/bin/realpath -e -- "$OMARCHY_PATH") != "$OMARCHY_PATH" ]] ||
! { [[ $command_source == "$OMARCHY_PATH/bin/$command_name" ]] ||
[[ $OMARCHY_PATH == "/usr/share/omarchy" && $command_source == "/usr/bin/$command_name" ]]; }; then
echo "OMARCHY_PATH does not match this Omarchy command." >&2
return 1
fi
}

omarchy_security_sudo_supports_no_update() {
local help
help=$(LC_ALL=C /usr/bin/sudo -h 2>&1) || return 1
/usr/bin/grep -Eq '^usage: sudo .*\[[^]]*N[^]]*\]' <<< "$help"
}

omarchy_security_revoke_sudo_timestamp() {
/usr/bin/sudo -k
}

omarchy_security_exit_with_revoked_sudo() {
local status=$1
local message=${2:-Could not invalidate cached sudo authorization.}

trap - EXIT HUP INT TERM
if ! omarchy_security_revoke_sudo_timestamp; then
echo "$message" >&2
(( status != 0 )) || status=1
fi
exit "$status"
}

omarchy_security_install_signal_exit_traps() {
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
}

omarchy_security_install_sudo_cleanup_traps() {
OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE=${1:-Could not invalidate cached sudo authorization.}
trap omarchy_security_run_sudo_cleanup_trap EXIT
omarchy_security_install_signal_exit_traps
}

omarchy_security_enable_no_update_sudo() {
local wrapper_dir="$OMARCHY_PATH/default/omarchy/sudo-no-update"
if ! omarchy_security_sudo_supports_no_update; then
echo "This sudo does not support --no-update; refusing mixed-trust work." >&2
return 1
fi
if [[ ! -f $wrapper_dir/sudo || ! -x $wrapper_dir/sudo ]]; then
echo "The command-scoped sudo wrapper is missing." >&2
return 1
fi
PATH="$wrapper_dir:$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
OMARCHY_SUDO_NO_UPDATE=1
export PATH OMARCHY_SUDO_NO_UPDATE
}

omarchy_security_run_sudo_cleanup_trap() {
local status=$?

omarchy_security_exit_with_revoked_sudo "$status" \
"${OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE:-Could not invalidate cached sudo authorization.}"
}

omarchy_security_assert_root_directory() {
local path=$1 expected_mode=$2 canonical owner actual_mode

[[ $path == /* && -d $path && ! -L $path ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$path") || return 1
[[ $canonical == "$path" ]] || return 1
read -r owner actual_mode < <(/usr/bin/stat -Lc '%u %a' -- "$path") || return 1
[[ $owner == "0" && $actual_mode == "$expected_mode" ]]
}

omarchy_security_prepare_private_root_directory() {
local path=$1 parent=$2

omarchy_security_assert_root_directory "$parent" 755 || return 1
if [[ -e $path || -L $path ]]; then
omarchy_security_assert_root_directory "$path" 700
else
/usr/bin/install -d -o root -g root -m 0700 -- "$path" || return 1
omarchy_security_assert_root_directory "$path" 700
fi
}
Loading