Commit Graph
2355 Commits
Author SHA1 Message Date
ZacharyZhang-NYandClaude Fable 5 ed9ef8bc04 Propagate key-trust failures and let doctor --fix heal a keyless mirror line
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:35:24 -04:00
ZacharyZhang-NY 8715c20af2 Merge remote-tracking branch 'origin/quattro' into quattro
# Conflicts:
#	default/pacman/pacman-stable.conf
2026-08-28 23:18:35 -04:00
Ryan Hughes 5925929cb6 Stop the browser policy EXIT trap from reporting a clean run as failed 2026-08-28 19:00:03 -04:00
Ryan HughesandGitHub 7d58bb9a62 Merge pull request #7972 from acrogenesis/harden-browser-policy-dirs
Stop world-writable browser policy directories
2026-08-28 18:00:20 -04:00
ZacharyZhang-NYandClaude Fable 5 39026fd41e Move the cn migration call into omarchy-migrate and guard downgrades onto pre-migrate trees
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 13:46:25 -04:00
ZacharyZhang-NYandClaude Fable 5 81c253b86e Restore executable bits on omarchy-cn-convert and omarchy-cn-revert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 13:46:11 -04:00
ZacharyZhang-NYandClaude Fable 5 f4d5fa4013 Run cn migrations in the update pipeline and serve package installs from omarchy cn update
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 13:35:25 -04:00
David Heinemeier HanssonandGitHub 06e32d243d Merge pull request #8198 from bastidotnet/harden-apple-brightness-device-cache
Validate the cached Apple-display device path before use
2026-08-28 14:42:22 +02:00
ZacharyZhang-NYandClaude Fable 5 f72a48f747 One-click convert between vanilla Omarchy and OmarchyCN
omarchy-cn-convert turns a package-based install into OmarchyCN in one
curl-able run: registry key + [omarchycn] repo, --ask=4 swap onto the
cn-built packages (Chinese menu/bindings and OMARCHY CN branding ride
the settings tree), [omarchy] mirror line, zh_CN locale, fcitx5-rime
seeding, CN Arch mirrors, and a plymouth rebrand, recording pre-state
in /var/lib/omarchycn/convert-state. omarchy-cn-revert restores that
state symmetrically and survives user-removed packages, self-deletion
mid-swap, and cosmetic plymouth failures after the state is cleared.
Checkout installs are refused toward the overlay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hLK3wsDuKVAC37GgDqg6H
2026-08-27 18:20:51 -04:00
ZacharyZhang-NYandClaude Fable 5 b183be2832 Put the OmarchyCN [omarchy] mirror first on the stable channel
The Gitea mirror db is registry-signed, so installs trust the registry
key right after the pacman.conf restore, and a cn migration does the
same on existing systems with an idempotent exact-line guard. The
insertion lives in cn/lib/mirror.sh so 'omarchycn doctor mirror --fix'
can restore the line after omarchy-refresh-pacman resets an overlay
system's conf. Only stable is mirrored: the Gitea repository name must
equal the pacman section name, and edge/rc stay on upstream directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hLK3wsDuKVAC37GgDqg6H
2026-08-27 16:20:48 -04:00
ZacharyZhang-NYandClaude Fable 5 111b857cda Add local Ollama as an AI Hub provider for claude-code
Ollama v0.14+ speaks the Anthropic Messages API natively, so the
registry drives it with a fixed 'ollama' token and runtime-listed
models: static_token skips secret storage everywhere keys resolve,
models_dynamic defers model validation to the live /api/tags list,
and the setup wizard installs the right GPU variant and starts the
service before listing local models. Doctor stops treating the
endpoint port as part of the hostname.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hLK3wsDuKVAC37GgDqg6H
2026-08-27 15:55:15 -04:00
ZacharyZhang-NYandClaude Fable 5 4b280a1767 Add DSH to the agent roster and Install > AI
Official npm @deepseek-ai/dsh (bin: dsh) rides the mise path like Dim.
dsh has no TUI, so the interactive launch is the documented 'dsh web'
and one-shot prompts use 'dsh --profile headless'. Auth is dsh-managed,
so it stays out of the AI Hub registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hLK3wsDuKVAC37GgDqg6H
2026-08-27 15:42:46 -04:00
d1845245d3 Unquote the new variables inside [[ ]]
AGENTS.md asks for unquoted variables inside `[[ ]]`, with quotes reserved for string literals being compared. The three conditions added here quoted them.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 21:40:31 +02:00
OmarchybotandGitHub 83881e979b Merge pull request #7984 from Chessing234/fix/webapp-name-slashes
Keep a web app name out of the launcher's directory structure
2026-08-27 19:40:46 +02:00
9ece53cede Prove the web app name guard, and reject before the icon is fetched
The slash guard was the only thing keeping a name out of the directory structure, and nothing tested it: deleting it left the suite green, because creating the launcher directly in the applications directory already makes the redirect fail on its own, with a raw bash error instead of the message. The assertion is on the message now, alongside the traversal case the guard actually closes -- on quattro a name of `../../../../escaped` writes its launcher clean outside the applications directory.

The interactive prompt read the name, fetched the favicon, wrote it and updated the icon cache before the name was ever checked, so a URL typed into the Name field left an icon behind on every attempt. Validating as soon as the name is read covers both paths from one place.

Removing by name also scanned unconditionally, so a machine with no applications directory printed a find error where omarchy-remove-gaming-xbox-cloud does not hide stderr.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:25:25 +02:00
David Heinemeier HanssonandGitHub 946704f309 Merge pull request #8224 from kevinmcconnell/once-requires-sudo
Run ONCE with sudo when installing
2026-08-27 17:36:31 +02:00
ea6ee9440a Add omarchy-crash-mute to mute and unmute one program
The mute was reachable only as `omarchy-toggle crash-ignore/<program>`, which asks whoever runs it to know the flag layout, to reduce a binary's path to the name the watcher keys on, and to have read the rule that a name climbing out of that directory writes an unrelated toggle. All of that was carried in the skill's prose, which is the wrong place for a rule that has to hold: prose is advice, and the thing being advised about is a name the crashed program chose.

So it is a command now. `omarchy crash mute hyprland` silences that program, `off` lifts it, `toggle` flips it, and no argument lists what is muted. It takes the binary's path as readily as the name and reduces it the way the watcher does, so the `Executable:` line from `coredumpctl` can be handed straight to it; it refuses what is not one component of a name, so it cannot be talked into writing outside its own directory whatever it is given; and it re-reads the flag afterwards and reports what is now true rather than what was asked for. The listing counts only regular files, because that is all the watcher honours -- anything else in there would read as muted while the crashes kept arriving. A leading `--` is consumed so a program named `-h`, which the router would otherwise answer with its own help, can still be muted.

The watcher gained an unrelated fix that this uncovered. Its fields are read with `IFS=$'\t'`, and tab is IFS whitespace, so an empty field collapsed into the next delimiter and shifted every field after it along one: a crash whose comm was empty had a path read as its pid and was discarded as somebody else's. A process can set its comm to nothing, so that was reachable. Empty fields now arrive as a dash like missing ones, and a dash joins the empty and dot cases that fall back to `unknown`.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 11:26:28 +02:00
8d14869689 Let a crash diagnosis mute that program's notifications
A crash that is understood is not a crash that stops: an upstream bug waiting on a release, a program that dumps core every time it exits. The diagnosis explains it once and the toast keeps arriving, and the only answer Omarchy had was Crash Capture, which turns off every program's notifications in order to silence one.

The watcher already resolves a name to dedupe on and announces that same name in the toast, so the mute is keyed on it: a flag file under toggles/crash-ignore/, written by the existing omarchy-toggle and read by the existing omarchy-toggle-enabled. One flag per name rather than one list, so `on` mutes, `off` un-mutes, and `ls -A` shows what is muted, with no new file format and nothing to parse. It is the executable's basename wherever one was recorded, falling back to the process name, which the kernel truncates to fifteen characters -- muting the truncated form would match nothing, forever, while looking like it worked.

The name is not always a name, though, and the mute turns it into a path. A program picks its own comm and prctl takes anything, including slashes, and the watcher falls back to comm whenever a crash carries no absolute executable. So it is stripped to its last component first: without that, `a/../bar-off` is a legal comm aimed at an unrelated Omarchy flag, letting a crashing program suppress its own notification and letting a user who accepted the offered mute hide their bar instead. Stripping does not always leave a component either -- `/` leaves an empty string, which is no kind of array subscript and no kind of toast, and `.` or `..` names a directory that omarchy-toggle would touch and report success on, leaving a mute that never matches. Both fall back to `unknown`, the word omarchy-agent-crash already uses for a name it does not have, and which mutes like any other.

The skill offers this at the end of a diagnosis and never runs it unprompted, which makes it the single change a diagnosis may make to a system it otherwise only reads. It tells the agent to use the name it was handed rather than re-derive one, since the watcher resolved that name already and the two agree for ordinary names and not for strange ones; a diagnosis started by hand from `omarchy agent crash <pid>` is given no name and gets the derivation instead. It also says to treat the name as hostile text rather than as a word -- it is whatever the crashed program's author called a file, so a single quote inside one closes the quotes around it and the rest runs as the shell -- and to check the flag arrived rather than assume it.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 10:32:10 +02:00
Luke ParkeandGitHub eb7ecd13f3 Keep Ori interactive when launched with a prompt (#8455) 2026-08-27 09:21:07 +02:00
77305ed3b9 Enable Dell XPS 13 sidecar speaker amplifiers (#7032)
The Dell XPS 13 DX13260 drives its two CS35L56 sidecar speaker amplifiers through a quirk that Linux only gains in 7.2, so until Arch ships that kernel the machine plays through one amplifier with no bass. The dell-xps13-sidecar-amps package selects the same driver path with a module override; this installs it on that exact machine and nowhere else.

The detector requires both the DX13260 product name and SKU 0E53, because the override forces a quirk value rather than merging into one, and a machine that gets it wrong loses whatever quirk the kernel would have chosen for itself.

Pacman registers a package even when its post_install scriptlet fails, so the leaf calls dell-xps13-sidecar-amps-apply itself instead of trusting the install to have applied: a failed cleanup or boot-image rebuild has to reach the caller rather than hide behind a package pacman considers installed. That is also why the migration marks reboot-required only after the apply succeeds — a migration that exits non-zero keeps no completion marker and retries the apply on the next run, even though pacman already has the package.

The leaf runs after intel/ptl-kernel.sh rather than beside the other Dell leaf at the top of install/hardware/all.sh, so its boot-image rebuild sees the Panther Lake kernel that step swaps in rather than the stock one it removes.

Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-25 22:29:27 +02:00
ZacharyZhang-NYandClaude Fable 5 aa87ed97c1 Dim prompts use the documented dim exec form
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 15:03:35 -04:00
acrogenesis bafc9a1000 Write browser theme colour through a passwordless helper
Managed policy dirs are enterprise trust roots, so they stay 0755 root:root. The menu path takes root for that one write through a sudoers glob of six hex digits, the same shape as omarchy-dns, and falls back to pkexec where the grant is not installed. Drop omarchy-browser-policy; a group member could plant any JSON, not just a colour.
2026-08-25 13:01:01 -06:00
ZacharyZhang-NYandClaude Fable 5 a3cd27dba3 Add Dim Agent CLI to the agent roster and Install > AI
npm dimcode (bin: dim) rides the mise path like Deep Code; entries
land alphabetically in the Default Agent and Install > AI menus.
CLI-only per dimagent.cn, login handled by the CLI itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 14:59:18 -04:00
acrogenesis bebe19bc70 Harden browser policy parent directories and validate theme RGB
install -d follows a planted ancestor symlink, and a writable parent can rename the managed leaf aside. chromium.theme is user-installed, so only a 0-255 RGB triple becomes a colour.
2026-08-25 12:10:08 -06:00
acrogenesis 95b791af16 Stop world-writable browser policy directories
Chromium managed policy is mandatory for every profile. World-writable
dirs let any local uid plant policy, including force-installed
extensions. Write goes through the omarchy-browser-policy group at 2775
so theme colour still works without other-write.
2026-08-25 12:04:02 -06:00
ZacharyZhang-NYandClaude Fable 5 0812b28527 Detect the missing official repo dbs by core.db, not any db
pacstrap leaves the offline repo's db in the target's sync dir, so
the any-db glob skipped the first-run update path on real installs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 13:50:11 -04:00
ZacharyZhang-NYandClaude Fable 5 9bf870449b Link kimi into ~/.local/bin after the official install
The installer only appends PATH to .bashrc, so the fresh binary was
invisible to the launching process and the login never started.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 13:46:23 -04:00
ZacharyZhang-NYandClaude Fable 5 9be4d24d6d App installs route first-time updates through omarchy-update
Direct pacman -Syu trips the update pacman guard once real upgrades
exist; the catalog installer now runs the upstream update pipeline
interactively and fails fast with instructions when non-interactive.
omarchy-cn-kimi-install gains its executable bit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 13:27:38 -04:00
ZacharyZhang-NYandClaude Fable 5 a317bfb36f Fix China app installs and wire the CN AI stack into Install > AI
Offline installs ship no pacman sync db, so yay resolved official
repo dependencies as AUR packages and every catalog install failed;
omarchy-cn-app-install now runs a full -Syu first (verified end to
end: WeChat builds and installs in the VM). Kimi Code gains a real
installer command reused by the harness registry, the default-agent
path, and new Install > AI entries (Kimi Code, Deep Code, AI Hub),
and the AI Hub wizard offers to launch native harnesses immediately
so their official CLI login runs, matching how Claude Code works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 13:17:52 -04:00
ZacharyZhang-NYandClaude Fable 5 f338f6cb15 Localize update confirmation and first-run notifications
The update prompt speaks Chinese and points What's new at the
OmarchyCN releases page; the Wi-Fi, update, keybindings, dictation,
and default-agent first-run notices follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 11:18:02 -04:00
Kevin McConnell 2c93e66b0c Run ONCE with sudo when installing
The install user is no longer in the docker group by default, so a bare
`once` cannot reach the Docker socket. Run with `sudo` instead.

The script already requires sudo to install the command and enable the
service, so we can safely use it for the initial command launch as well.
2026-08-25 16:12:10 +01:00
ZacharyZhang-NYandClaude Fable 5 2eff2a37b3 Localize dynamic bind templates and the Super+K matcher
Workspace, group-window, and bar-panel loops emit Chinese
descriptions; omarchy-menu-keybindings' merge list and priority
patterns follow them, so combined chords and curated ordering
survive localization. Affected tests updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 11:09:05 -04:00
ZacharyZhang-NYandClaude Fable 5 907f7b81aa Fix kimi install error masking, prompt flag, and setup default hop
The official installer downloads to a temp file so curl failures
cannot be masked by the pipe; Kimi prompts use --prompt per the CLI
reference; the AI Hub wizard now offers omarchy-cn-ai-default for
claude-code/codex profiles so the combo really becomes the default
agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 11:01:50 -04:00
ZacharyZhang-NYandClaude Fable 5 0778554984 Add Kimi Code and Deep Code to the default agent roster
kimi installs through its official script rather than mise;
deepcode rides the existing mise path as npm:@vegamo/deepcode-cli.
Both gain launch branches and Default Agent menu entries, plus an
AI Hub entry for provider combos (Kimi/DeepSeek/Z.AI/MiniMax over
Claude Code) via the omarchycn setup wizard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 10:55:45 -04:00
ZacharyZhang-NYandClaude Fable 5 763ebb83c3 Purge remaining references to removed western apps
The quattro upgrade script no longer registers the HEY mailto
handler, installer tips stop naming LibreOffice, and the hotkeys,
GUIs, and web-apps manuals drop the entries for removed apps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 10:30:28 -04:00
ZacharyZhang-NYandClaude Fable 5 6d8acfca9f Drop western default apps from the CN default set
Basecamp, Discord, HEY, Google Contacts/Maps/Messages/Photos,
WhatsApp, and X lose their shipped launchers, webapp keybindings,
and the whatsapp-slim extension; aether, libreoffice-fresh, and
obs-studio leave the base package set. The HEY mailto handler and
its registration go with them. Preinstall add/remove lists, the
launcher hide list, and chromium flags stay in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 10:23:52 -04:00
4cd8a081cb Fix Codex usage collection on 0.149 (#7649)
* Fix Codex usage collector approval policy

* Capture codex argv with boundaries in the scanner test

The stub joined its arguments with "$*", so the assertion compared one
flattened string and could not tell five arguments from fewer containing
spaces. Passing "-s read-only" and "-a on-request" as single arguments --
which codex rejects as an unexpected argument -- passed the test. NUL
separation and an array comparison keep the boundaries the assertion is
about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 16:07:17 +02:00
bastidotnet fe56d68e90 Validate the cached Apple-display device path before use
The cached device path was trusted for merely existing, not for being a hiddev node, and fell back to a predictable /tmp path when XDG_RUNTIME_DIR was unset. Validate the cache shape (hiddev char device) and cache only under the user-private runtime dir; asdcontrol already gates non-Apple devices downstream, so this is defense-in-depth in the layer Omarchy owns.
2026-08-25 13:24:00 +02:00
23dab9ec4d [Security] Stop the FIDO2 setup staging its authfile at a predictable /tmp path (#7904)
* [Security] Stop the FIDO2 setup staging its authfile at a predictable /tmp path

pamu2fcfg wrote to /tmp/fido2 and the registration was then moved into place
with `sudo mv`. Any other local user can pre-create /tmp/fido2, and rename(2)
does not dereference the final component, so the privileged move installed the
attacker's symlink itself as pam_u2f's global authfile -- a file consulted by
`sufficient` lines in /etc/pam.d/sudo and /etc/pam.d/polkit-1.

The same move also carried the staged file's ownership into /etc, so on every
install to date /etc/fido2/fido2 is owned by the invoking user at mode 0644.
That needs no attacker: anything running as that uid can add its own credential
and satisfy the machine's sudo prompt without root.

Stage under mktemp and hand the bytes to `install` instead, so the authfile is
always a fresh root-owned regular file rather than an inode a non-root user
still controls. Guard the already-registered check with -L, which -f would
otherwise follow, and reject a symlinked /etc/fido2 in the remove path for the
same reason. A migration takes ownership of authfiles left behind by the old
code; it reports a symlink rather than repairing one, since chown would follow
it and removing it would strip sudo from anyone whose only credential is the
token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6WagC5iUigCGoK1tQeFz

* Install the FIDO2 authfile with -T and refuse a non-regular path

`install SRC DEST` without -T treats an existing directory at DEST as a
destination directory: it drops the credential inside as
/etc/fido2/fido2/tmp.XXXX, exits 0, and setup reports a successful registration
while PAM goes on reading a path that is not a file. -T makes that an error.

The already-registered check has the same blind spot from the other side. -f
follows symlinks, so it reads a symlinked authfile as a registration and leaves
it in place, and is false for a directory, so it tries to register over one.
Only a regular file is a pam_u2f authfile; anything else is now refused with the
same advice to remove it and set FIDO2 up again.

The test deleted every staged path that fell outside its scratch directory,
taking the path from the script under test and already resolved through any
symlink -- so a script staging through one would have named a file of the user's
and had it unlinked. It now unlinks only a file its own stub wrote into.

On a machine that already has /etc/fido2/fido2 the staging assertions cannot run
at all, and the file used to pass without exercising one of them. That branch now
asserts what the host state promises instead: a regular authfile still has to be
recognised as a registration and left alone, and anything else has to be refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Replace the FIDO2 authfile inode rather than chowning it in place

Permission is checked at open(2), not at write(2), so a descriptor the
registering user opened on the authfile while it was still theirs stays writable
through chown and chmod alike. pam_u2f resolves /etc/fido2/fido2 to that same
inode, so the repair left the account it authenticates able to append a
credential it controls -- the exact state the migration exists to end, now
recorded as migrated and never revisited.

Installing a fresh root-owned copy and renaming it over the path leaves any such
descriptor writing to a file nothing reads. Credit to #7703, which reached the
same conclusion independently. An interrupted run heals: the staged copy is
root-owned 600 and inert, no marker is written, and the next run replaces it.

A directory or device at the authfile path is no more ours to rewrite than a
symlink is, and chmod 600 on a directory would only make it untraversable, so
both are now reported rather than repaired.

The repair had no test, because it names an absolute path no unprivileged suite
can write. It is exercised through a scratch copy with that one literal
retargeted, rather than by reading the path from the environment: the migration
hands `install` and `mv` root, and an operand the caller can choose is a
privileged write to anywhere. The copy is only as honest as the substitution, so
the test fails if the migration stops naming the path exactly once.

Covered: the no-op on a machine that never registered a key, which must not cost
a password prompt; the repair itself; the new inode; the absence of a staged copy
afterwards; a second account finding it done; and the symlink and non-regular
cases. Each assertion was checked against a mutation that defeats it -- notably a
repair with the right install call, mode, content and cleanup that writes through
the old inode, which only the inode assertion catches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Finish hardening FIDO2 authfile installation

* Guard the FIDO2 directory and the stage path the setup writes through

install -d follows a symlink at /etc/fido2 and applies the mode and ownership to whatever it points at, so the credential would be staged and published inside the link target and that directory silently reopened to root:root 755. The leaf guard above it only covered fido2 itself, and this is the same threat omarchy-remove-security-fido2 already names on its side.

mktemp's output is an operand for a privileged tee, chmod, mv and rm. The migration validates it before any of them run; the setup did not, so take only the name it asked for there too.

The suite was guarded on the host's own /etc/fido2/fido2 and exited early when one existed, which meant the staging assertions asserted nothing on exactly the machines that use FIDO2. Drive a retargeted copy the way the migration suite already does, so every branch is a fixture and all of them run everywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop the FIDO2 migration recording a repair it never made

omarchy-migrate writes the per-user completion marker on any zero exit, so the two states this migration cannot repair got one line in the update terminal and were then silenced for good: no login notice, no re-run, the migration recorded as done having repaired nothing. Those are precisely the machines where the authfile may already be under someone else's control, so raise them through omarchy-notification-send as well, where they outlive the scrollback. Delivery is best-effort: a machine with no user bus or no notification server must not abort the migration and take every later one with it.

The early exit had the same shape of problem. It read the authfile unprivileged, and the old setup created /etc/fido2 with `sudo mkdir -p`, which took the union of the caller's umask and sudoers' 0022 — so registering under `umask 077` left the directory mode 0700 with the user-owned authfile still inside. Absence and "cannot look" are the same answer to those tests, and the migration exited 0 and marked itself complete. Ask root whether a registration is actually behind an untraversable directory before reopening it, so an aborted setup that left an empty directory, or one an administrator keeps private, does not have its mode widened and its group and special bits discarded for a repair it does not need. A machine that never set FIDO2 up has no directory here and still reaches exit 0 without a password prompt.

The notification assertion checks argument shape rather than a substring of the command line. The glyph is a private-use codepoint, and losing it shifts every argument left: -g swallows the headline, the body becomes the title, and the message goes out with no description — which a substring match reads as a pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Cover the FIDO2 removal's symlink guard

The -d to -e || -L change is load-bearing for the threat its own comment names — a dangling link at /etc/fido2 that -d reads as absent, left for the next setup to install an authfile through — and it was the one part of this work with no test behind it. Name the directory once so the suite can retarget a copy, the same seam the setup and migration suites use, and assert both halves: the link goes, and the directory it pointed at does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 12:09:17 +02:00
9285b19d6a [Security] Stop USB device names from being executed as Hyprland Lua (#8129)
* Stop device names from being executed as Hyprland Lua

Hyprland input-device and monitor names come from USB descriptors and
hyprctl output, so they are attacker-influenceable, yet the toggle and
monitor commands interpolated them straight into hyprctl eval and into
generated Lua that Hyprland re-executes on every reload. The input-device
toggle keys are bound with locked = true, so a malicious USB name reached
Lua code execution from the lock screen; a persisted disable made it run
on every start. This closes that class everywhere it appeared.

- The touchpad/touchscreen disable is now the device name in a plain-text
  sidecar file, read back by a packaged Lua module on reload, never a
  generated Lua file. hyprctl eval Lua-quotes the name and control
  characters are rejected outright.
- Dropped the shipped *-disabled.lua templates so nothing seeds a
  disabled state to /etc/skel, making the name file the single source of
  truth read from a hardcoded ~/.local/state to match the sibling tools.
- The reload loader excludes those two legacy filenames, so a leftover
  generated *-disabled.lua on a not-yet-migrated install can never be
  sourced as code again; a migration then recovers the device name from
  it and deletes it, sanitizing installs that ran the vulnerable version.
- All four monitor scripts (internal, mirror, clamshell, scaling) now
  validate an output name against a plain-connector-name pattern before
  writing it as Lua, closing the same latent pattern in the siblings.
- paths.lua treats a set-but-empty XDG_STATE_HOME as unset, matching the
  bash side so state is never read from the filesystem root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144ZDt44vtxjyF8j9Y88NrM

* Let a failing Lua assertion fail the test

lua discards the status of a chunk read from stdin, so a blown assert printed its traceback and still exited 0: the surrounding `set -euo pipefail` never fired and the following `pass` printed `ok`. Every Lua block in these two files was unenforced, including the assertion that a quoted `hyprctl eval` cannot reach `os.execute` and the negative control that proves the test can detect the injection at all. Passing the chunk as a script argument makes lua report the failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Re-apply a recovered input-device disable to the running session

The package hook reloads Hyprland during `omarchy-update-system-pkgs`, before `omarchy-migrate` runs, and at that reload the generated Lua is already excluded while the name file does not exist yet — so a touchpad or touchscreen the user had switched off comes back on, and stays on until their next login. Reload once more once the name has been recovered, which is the same path a login already takes to read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <codex@openai.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Omarchybot <omabot@omarchy.org>
Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-25 11:03:12 +02:00
4637735aa2 Pin trusted PATH in privileged DNS helper (#8172)
* Pin PATH to trusted dirs when omarchy-dns holds root

A dev link prepends a user-writable checkout bin/ to sudo's secure_path,
so the passwordless `omarchy-dns Cloudflare` sudoers rule lets root
resolve a bare helper (dirname, install, tee, nmcli, ...) out of that
checkout — turning checkout-write access into arbitrary root execution.
Pin PATH to trusted system directories once EUID is 0, leaving the
unprivileged wrapper phase free to locate sudo/pkexec on the caller's
PATH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUWoHbBoKMjsjV6X3nu1H5

* Assert the trusted-PATH pin is gated on root, not merely present

The EUID assertion matched `(( EUID == 0 ))` anywhere in the file, and require_root has carried that exact test since long before the pin existed. Deleting the pin left the assertion passing, so it stood for nothing: a run with the pin neutered reached the behavioural probe with both greps green. Anchor on the unindented guard and require the pin to be the line it opens, which no other construct in the script satisfies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Skip the DNS trusted-PATH probe where user namespaces are unavailable

`fail` ends the file, so a sandbox or hardened kernel that refuses unprivileged user namespaces did not just lose the probe — it took the two elevation assertions below it down as well, reporting a product defect where there was only a missing capability. The non-graphical suites are meant to run on any machine and treat a skip as a passing test, the way require_compositor and plugin-add-test.sh already do. Gate the probe on the namespace it needs and say so when it is absent; the static checks above and the elevation checks below run either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 09:37:55 +02:00
68ab12f77d Share the git URL check, and refuse the transports Omarchy does not clone from (#8174)
* Share the git URL check between theme-install and plugin-add

Both commands clone a URL a stranger can choose, and each carried its own copy of the rule that refuses a git option or a `<helper>::<address>` transport helper before cloning. Two copies of a security check drift: the second one arrived four months after the first, and only because someone went looking for it.

The rule now lives in omarchy-git-url-check and the callers ask it. Its absence refuses the URL rather than waving it through, since the callers read a non-zero status as a refusal and a missing command exits 127.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Refuse a git URL naming a transport Omarchy does not clone from

`<helper>::<address>` is only one of the two ways a URL reaches a remote helper. git also resolves 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` arrive at the same helper while only the first was refused.

That shape cannot be refused outright, because it is also how every legitimate URL arrives, so the scheme is checked against the transports git still connects itself. `git+ssh` and `ssh+git` are on that list: they are spelled like a helper and read as plain ssh, and leaving them off would refuse a URL that clones today. `ext` and `fd` are off it deliberately -- git ships a helper for each, and `ext` runs whatever command the URL carries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 09:10:20 +02:00
BastiandGitHub 30471bf35a Guard plugin-add against git transport-helper URLs (match theme-install) (#8067)
* Guard plugin-add against git transport-helper URLs

omarchy-plugin-add cloned a user-supplied git URL without the
transport-helper guard that omarchy-theme-install already applies
(added in #7884, which did not touch plugin-add). Port that guard
(reject ext::/fd:: and leading-dash forms, keep https/ssh/scp-style
incl. IPv6) and add a regression test. Stock systems are unaffected
(git default protocol.ext.allow=never); this removes the silent
dependency on that default and aligns the two install paths.

* Test the plugin-add guard's leading-dash arm via the gum input path

The prior leading-dash cases only exercised the argv option parser, not
the guard (removing the guard's -* arm left them green). Drive a dash
value through the interactive gum prompt under a pty so the post-input
guard is actually covered; skip cleanly where util-linux script is
unavailable.
2026-08-25 08:29:48 +02:00
ZacharyZhang-NYandClaude Fable 5 6ce3bbf8d6 Merge upstream sync b86d4505 (README resolved toward CN rewrite)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-25 01:18:23 -04:00
ZacharyZhang-NYandClaude Fable 5 9546fe98b9 Update: refuse pre-channel downgrades; doc docker restore scope
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-24 23:24:39 -04:00
ZacharyZhang-NYandClaude Fable 5 2f5f02d3e5 Update: stable-channel empty tag list fails with message, not silently
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-24 23:21:11 -04:00
ZacharyZhang-NYandClaude Fable 5 eb149f30c4 M5 review fixes: key download step, honest update paths, ordered checklist, channel semantics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-24 23:19:26 -04:00
ZacharyZhang-NYandClaude Fable 5 786544edf3 Add release checklist, sync SLA, security response, channel command
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-24 22:17:06 -04:00
ZacharyZhang-NYandClaude Fable 5 9f78e1530f cn commands: use omarchy-cmd helpers per bin style rule
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-24 21:16:54 -04:00
ZacharyZhang-NYandClaude Fable 5 0aaa770561 Wizard: explicit step error propagation, honest final diagnosis
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKxGW1raAWaqeU8WdHsMsp
2026-08-24 21:08:58 -04:00