Skip to content

Add claude-desktop, tracked from Anthropic's signed apt index - #274

Open
evindor wants to merge 3 commits into
omacom:masterfrom
evindor:add-claude-desktop
Open

Add claude-desktop, tracked from Anthropic's signed apt index#274
evindor wants to merge 3 commits into
omacom:masterfrom
evindor:add-claude-desktop

Conversation

@evindor

@evindor evindor commented Sep 2, 2026

Copy link
Copy Markdown

What

Adds claude-desktop, Anthropic's official Claude desktop app (Chat, Cowork, and Claude Code), tracked from Anthropic's own Debian repository.

Anthropic ships the Linux app only as a .deb from their apt repository, and their docs send Arch users to the CLI instead. This package unpacks that .deb and nothing else — no patching, no rebuilding, no bundled extras.

Why not the AUR

Same reasoning as openai-codex-desktop: releases land several times a week (six in the last three weeks), and tracking the vendor's index directly keeps up without waiting on an AUR maintainer.

There is a second reason here. Anthropic signs their apt index, so this package can do something the AUR route can't: refuse to accept a checksum that Anthropic hasn't signed.

How the checksums are trusted

.omarchy/upstream.sh walks a chain that starts at a key committed in this repo:

bundled key, pinned by fingerprint  ->  signs InRelease
InRelease                           ->  hashes Packages
Packages                            ->  hashes each .deb

Every checksum that reaches the PKGBUILD arrives under Anthropic's own signature. A break anywhere fails the sync instead of proposing a checksum nobody vouched for. Verified failing closed against: a fingerprint that doesn't match the anchor, a substituted key, a missing key, and an attacker-modified Packages served under a genuine InRelease.

The key is committed rather than fetched, so rotating it is a reviewed change to this package rather than something the server can do to us. Its fingerprint 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE was cross-checked against four independent sources: the published install docs, downloads.claude.ai/claude-desktop/key.asc, the InRelease signature itself, and the copy embedded in the .deb's own postinst.

min_release_age: "24h" is set, so an automated sync will not ship a release that upstream hasn't had a day to pull. The hook reports published_at from the pool's Last-Modified, and falls back to the newest release that has already cleared the window rather than stalling on a fresh one.

What is dropped from the .deb

The .deb's maintainer scripts are not reproduced. They do three Debian-specific things, none of which applies here:

  1. register Anthropic's apt repository and an unattended-upgrades snippet,
  2. install an AppArmor profile gated on Ubuntu 24.04's userns restriction,
  3. register a GNOME Shell search provider.

The payload itself is confined to /usr and carries one setuid binary, chrome-sandbox — Chromium's sandbox helper, shipped 4755 the way every Chromium-based package does.

Keyring on Hyprland

Chromium cannot reliably infer the Secret Service password-store backend from a Hyprland session even when GNOME Keyring is already providing it, so the app intermittently decides no keyring is available, refuses to persist the sign-in, and shows "Your sign-in won't be saved on this device". This is the same thing hermes-desktop hit, and 246eea9 is the same fix — by flag rather than env var, because that is what this app reads.

The .deb ships /usr/bin/claude-desktop as a symlink to the Electron binary; this replaces it with a launcher that names gnome-libsecret. Three cases are left alone: an explicit --password-store from the caller, KDE (the app appends --password-store=kwalletd6 itself and carries fallback logic for a KWallet with no wallet), and CLAUDE_DESKTOP_PASSWORD_STORE=none. libsecret is already a dependency.

I hit this on a fresh install: the toast appeared, yet the keyring was healthy the whole time — gnome-keyring running, owning org.freedesktop.secrets, collection unlocked, and a Claude Safe Storage item created one second after first launch. "Not reliably" is exactly right; it is a race, not an absent keyring.

Cowork

Cowork boots its agent in a QEMU VM. Upstream ships that stack under Recommends:, which apt installs by default, so it is a hard dependency here to give the same working-out-of-the-box Cowork that Debian and Ubuntu users get. Happy to move qemu-system-x86/edk2-ovmf to optdepends if you'd rather not pull a virtualisation stack onto every machine — virtiofsd does need to stay a real dependency, since the app resolves it from the system before its own bundled copy.

Two symlink shims map Debian's firmware and virtiofsd paths onto Arch's. aarch64 needs no firmware shim; edk2-aarch64 already installs at the path the app opens.

Testing

  • bin/sync-upstream self-test passes.
  • bin/sync-upstream claude-desktop reports correctly against the checked-in pin, and rewrites pkgver, pkgrel and both checksum arrays correctly when the pin is behind.
  • Builds clean for x86_64 on Omarchy 4.0.2; the resulting package contains only /usr, the expected setuid chrome-sandbox, the two shims, and the launcher.
  • The launcher's branches were exercised against a stub: default, URL passthrough, both --password-store spellings, CLAUDE_DESKTOP_PASSWORD_STORE set and none, KDE and Plasma, and an unset XDG_CURRENT_DESKTOP.
  • The pinned 1.40609.1 checksums were verified by hand against the signed index and against a downloaded .deb.

I don't have aarch64 hardware, so that build is unverified beyond the checksums coming from the signed index.

Follow-up, if this merges

This only makes the package installable (omarchy pkg add claude-desktop). It does not add a menu entry — Install → AI lists ChatGPT Desktop, Dictation, Grok Bot, LM Studio and Ollama, and Claude would still be missing from it.

That belongs in omacom/omarchy rather than here, and needs no new script — the Grok Bot / LM Studio shape covers it, since unlike ChatGPT there is nothing to launch through uwsm-app:

"install.ai.claude": {"icon":"󰛄","label":"Claude Desktop","when":"! omarchy-pkg-present claude-desktop","action":"omarchy-install-and-launch 'Claude Desktop' claude-desktop com.anthropic.Claude"},

com.anthropic.Claude is the desktop-id the package installs, and 󰛄 is the glyph the default-agent menu already uses for Claude. It has to land after this one, since both the when guard and the action name a package that does not exist yet. Happy to open that separately once this is in, or to drop it if you would rather Claude Desktop stay install-by-name only.

🤖 Generated with Claude Code

evindor and others added 2 commits September 2, 2026 12:49
Anthropic ships the Claude desktop app for Linux only as a .deb from
their own apt repository, and their docs point Arch users at the CLI
instead. This unpacks that .deb and nothing else.

Tracked from the vendor repository rather than the AUR for the same
reason as openai-codex-desktop -- releases land several times a week --
and for one more: Anthropic signs their apt index, so .omarchy/upstream.sh
can refuse any checksum that signature does not cover.

The trust chain runs from a key committed beside the hook and pinned by
fingerprint, through the InRelease it signs, to the Packages index that
InRelease hashes, to the .deb hashes Packages carries. A break anywhere
fails the sync rather than proposing an unvouched checksum. Committing
the key rather than fetching it makes rotation a reviewed change here
instead of something the server can do to us.

min_release_age is 24h, so an unattended sync will not ship a release
upstream has not had a day to pull; the hook dates releases from the
pool's Last-Modified and falls back past a quarantined one.

The .deb's maintainer scripts are not reproduced: they register an apt
repository, install an AppArmor profile gated on Ubuntu's userns
restriction, and register a GNOME Shell search provider. Two symlinks
map Debian's virtiofsd and OVMF paths onto Arch's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Chromium cannot reliably infer the Secret Service password-store backend
from a Hyprland session even when GNOME Keyring is already providing it,
so the app intermittently decides no keyring is available, declines to
persist the sign-in, and shows a toast saying so. hermes-desktop hit the
same thing; this is the same fix (246eea9), by flag rather than env var
because that is what this app reads.

Replace the /usr/bin/claude-desktop symlink with a launcher that names
gnome-libsecret. An explicit --password-store from the caller wins, KDE
is left to the app's own kwalletd6 handling, and
CLAUDE_DESKTOP_PASSWORD_STORE overrides the choice. libsecret is already
a dependency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gpg --batch --verify exits 0 on a clearsigned document that carries unsigned text outside the armour, and the SHA256 lookup takes the first match in the file, so anyone able to serve InRelease could prepend a block naming whatever Packages hash they liked and the sync would carry it through to the PKGBUILD -- the one thing the pinned key is there to prevent. Writing the signed plaintext out with --decrypt and reading that back gets only the bytes the signature covers, and fails closed when there is no valid signature at all rather than falling through to the served text.

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

Copy link
Copy Markdown
Collaborator

Reviewed, and the mechanics are in better shape than most packages of this kind — but the trust chain had a hole in exactly the place the PR body says it does not. I proved it, fixed it, and pushed 571cc14. Separately, whether Omarchy should host this at all is a decision only the maintainer can make, and it is a bigger question than the packaging.

The signature chain was bypassable

The claim is that no checksum reaches the PKGBUILD without Anthropic's signature. As opened, that did not hold. .omarchy/upstream.sh:58 verified the signature with gpg --batch --verify "$WORK_DIR/InRelease", and then line 73 ran awk over the same served file — not over the bytes the signature covers. A clearsigned document can carry arbitrary unsigned text outside the armour, gpg --verify still reports a good signature, and the SHA256: lookup takes the first match it finds.

I tested it against Anthropic's real InRelease, prepending one unsigned block:

gpg --verify exit: 0            (accepted)
hash the sync would trust: eeeeeeee…      <- attacker's
genuine hash:              736dd28f…

Anyone able to serve InRelease could name a Packages hash of their choosing, serve a matching Packages, and have its .deb checksum written into the PKGBUILD under a "verified" signature — which is precisely the compromise the pinned key exists to stop. The listed failure cases in the PR body (wrong fingerprint, substituted key, missing key, modified Packages under a genuine InRelease) are all genuinely closed; this one is cheaper than any of them and was open.

571cc14 writes the signed plaintext out with gpg --output … --decrypt and parses that instead. Same test after the fix:

gpg --decrypt exit: 0
hash the sync would trust: 736dd28f…      <- genuine
gpg --decrypt on unsigned text: exit 2    (fails closed)

and the fixed hook still produces the right answer against the live index — pkgver 1.40609.1 with both checksums matching what is already pinned in the PKGBUILD.

The rest of the chain is sound, and I walked all of it on a worker rather than reading it: the committed key is a single key whose fingerprint is 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE as pinned; GNUPGHOME is a fresh directory so the user's keyring is never consulted; InRelease verifies against that key; main/binary-amd64/Packages hashes to 736dd28f…, exactly what InRelease signs for it; and the PKGBUILD's sha256sums_x86_64 (80182e85…) is the SHA256 the signed index carries for claude-desktop 1.40609.1 amd64 at the pool path the PKGBUILD constructs. I downloaded the .deb and it hashes to that value. The pin is genuine.

One weaker link left, worth knowing rather than fixing: published_at comes from the pool's Last-Modified header (upstream.sh:147-156), which is unsigned and server-controlled. min_release_age: "24h" is therefore a courtesy delay, not an authenticated one. The fallback logic itself is right, and bin/sync-upstream:409-425 independently backstops it.

What it installs, and where

Payload is confined to /usr — I listed all 3117 paths and there is nothing outside it. /usr/lib/claude-desktop/claude-desktop exists, so the launcher's target is correct; /usr/bin/claude-desktop and chrome-sandbox exist at the paths package() manipulates; the desktop id is com.anthropic.Claude, matching your follow-up snippet; and /usr/share/doc/claude-desktop/copyright is there for the licence install.

No file conflicts: Arch's virtiofsd owns /usr/lib/virtiofsd only and nothing owns /usr/bin/virtiofsd, and edk2-ovmf owns usr/share/edk2/x64/OVMF_{CODE,VARS}.4m.fd — the exact targets your symlinks point at — but not the two root-level names you add. Both shims work. Worth naming anyway: installing a Claude app puts a virtiofsd on every user's $PATH system-wide, and would become a hard conflict the day Arch ships that path itself.

chmod 4755 on chrome-sandbox is how every Chromium package does it and the payload already carries that mode. It still means Omarchy's mirror would ship a setuid-root binary it did not compile, which belongs in the decision below rather than in the diff.

bsdtar -xOf "${deb}" data.tar.xz (line 118) hard-codes one of several compressions deb(5) permits. The payload is data.tar.xz today — I checked the ar headers. If Anthropic switches to zstd, the first bsdtar fails, package() does not run under pipefail, and the second bsdtar succeeds on empty input — but chmod at line 124 then fails on the missing chrome-sandbox, so it breaks the build rather than shipping an empty package. Fails safe; still worth globbing.

Smaller things

  • bash is not in depends, though claude-desktop-launcher.sh is #!/bin/bash. openai-codex-desktop declares it (PKGBUILD:18). Omarchy's base install masks it, but the precedent package in this repo gets it right.
  • claude-desktop-launcher.sh:11 says CLAUDE_DESKTOP_PASSWORD_STORE overrides the choice, but the KDE branch execs at lines 24-26 before line 28 reads it, so a KDE user cannot use the variable. Either the comment or the ordering wants adjusting. Every other branch checks out — explicit --password-store in both spellings, URL and space-containing arguments, no arguments, unset XDG_CURRENT_DESKTOP; quoting and exec preserve arguments correctly.
  • Codex flags that edk2-aarch64 (depends_aarch64, line 74) may not be resolvable from the Arch Linux ARM repositories, which would block the declared aarch64 build. I could not verify that from here — no aarch64 worker — so treat it as a pointer rather than a finding, but it is worth checking before this ships for ARM, especially as you note you have no aarch64 hardware.
  • Versus openai-codex-desktop: that hook reads an unsigned Packages directly (.omarchy/upstream.sh:40), has no release-age gate, and also hard-codes data.tar.xz. With 571cc14 this is the stronger of the two by a clear margin.

The decision, which is not mine

omarchy-pkgs builds packages and hosts the resulting binaries on Omarchy's own mirror. That makes this redistribution of Anthropic's proprietary .deb, not publication of a recipe — which is the substantive difference from the AUR route, and from how license=('LicenseRef-Proprietary') reads at a glance. Copying the vendor's copyright file into /usr/share/licenses records the terms; it does not grant permission to mirror the payload. Whether Anthropic's terms permit Omarchy to host and serve a 166 MB proprietary binary is a question for a person, not a reviewer, and the same call covers shipping a setuid-root helper Omarchy did not build. I am flagging it rather than arguing it either way — but it should be settled before merge rather than after, because the mirror is the part that is hard to walk back.

Pushed: 571cc14claude-desktop: parse the verified InRelease, not the file as served. One hunk in .omarchy/upstream.sh, nothing else touched.

Second opinion: codex at xhigh reviewed this independently and rated the InRelease parsing High for the same mechanism. It ran after my fix was already on the branch and can read this session's workspace, so its independence there is not guaranteed — the empirical test above is the evidence, not the agreement. It did contribute findings I had not reached: the missing bash dependency, the KDE/CLAUDE_DESKTOP_PASSWORD_STORE ordering mismatch, and the edk2-aarch64 availability question. It reached the licence and setuid concerns separately.

Waiting on the maintainer for the hosting decision, and on you for the smaller items.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants