Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions pkgbuilds/claude-desktop/.omarchy/anthropic-release-signing.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBGnK73ABEACnbytJXkjweYrwIr0aLEFRlH+C0nF44KxFc7gQmJ6PjSPMGZAD
dxZcaixU7zZl8WxEpVO0wLmIH8cf2zGOdyuZg1Yaugk1vHb2b8WBhAGCQJdPgB8W
XquedepEYtk56uP/gCoTjJDUZluEGBHnlnuujSJ4orxEdhSykEoAUfJZGEILPpMd
bphFt/Sn+Eb/TxM5jpKPdwnv8AShNF/1mZU1fWTQq9tRKJUakZj04gdaDFElQXak
CtTij+GT6yoYCARSHwGO+PC/Pr6q4tc+D7LRjxSBvUWDoFSmlqb/PJ1hj9D/7I2O
e4XXniAPWMR56KvxHlzOzrNQdJujbJdSkCwh1ZijkSd3y8ayW5WYUTGdRab99NUw
agzlabe/VVF6kzJ0Scn5q3PihB2Y9Bwo0CKnkYk7a7KT77EWv0Kkq+VHmOtqX3a2
hhX+b6a6ve9rzJ1qZYGj+obv/C3Sx1LzUjAfqVy7RJDf2uAoP5t2g8u/TkSpUxhM
VEjZBkSxYZhMyzQM6t8IgkUfnSrIPTHixbDWARZ4beMOBjxyPZK1nP7OOrNR3TkK
JtwLMQAabURCDnL0PjS0iwBTU4jtumBD1XSULyWuoTvMljrpQr1nV1oDyOt0OLqa
KA2McWtd9PdXhC8y2EIg7TmrTlJLfHYbdmkiCYj4J49Q8HWkN/6WE+RTUwARAQAB
tD5BbnRocm9waWMgQ2xhdWRlIENvZGUgUmVsZWFzZSBTaWduaW5nIDxzZWN1cml0
eUBhbnRocm9waWMuY29tPokCUQQTAQoAOxYhBDHd3iTd+rZ59C170rqpKf8afsrO
BQJpyu9wAhsPBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJELqpKf8afsrO
l5IP/2I8X1dFy5xYczWB/coIxGjuzS/V6ByZGZZEJsbr04pmuHiFUykJqPGWGQ6q
U0YF5iEwvEkaagS5m7DzhSEf3FM3Cgafax/6d70tar9Vr1D+w6uPfxetu7u/WYJp
aolIsdh5fTrBh9zSM1Njl8FM8wG8CwZQjS33Oa7d8cwRkgdUWbt6LXgz+cTQNuBn
BgW6Ks7oZFI25dfu0ojDR+aDFJg4+4wZoyDLPvJz1SIrJ5WFGs67zsx9SfS3yZnf
XKmBe+f0dUy+GJ2nFZrXFf99+c0dPEHYO8DCeAHZizjkFrdYtUHdDU0YDYEGkLJa
bE+pgcpkHf5EvsZzHsyDbl95W/eh7pcXMbwkN+W4CBYUE9X4uHhqzWaC5yAVRWUA
1BJ9V4LjZfHPLEJt0I3TxzXiEg9/BVeaTYq9RjaxIFo9Nfk158HqJY6SA5jslBlx
Gv/No8u+xVcze2UJyGVfEIUfm92+0UAIkny3+5cuVV0ICzJxXlXj0CnLM9Lt50wE
p3suVwuBEviCbZ08eAH1Ht8gbBdSsiOkIU8CX3v/scwHHx5q0+NBL6xLrQObg13a
tRXBlKObfElkPN3lTUbUnJOW4U8uSjH8VRP+AujKWMDFe7x0zCs+iYY1mTOvbrTS
9n3CmZUmbynZ+E/QWNENpW/pDNZdWFy43PASmML5FHu4m9Sn
=oqMI
-----END PGP PUBLIC KEY BLOCK-----
5 changes: 5 additions & 0 deletions pkgbuilds/claude-desktop/.omarchy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"source": "local",
"release_ring": "fast",
"min_release_age": "24h"
}
190 changes: 190 additions & 0 deletions pkgbuilds/claude-desktop/.omarchy/upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/bin/bash
# Anthropic ships the Claude desktop app from its own Debian repository, and
# signs that repository's index. Reading the index costs three small HTTP
# requests instead of two half-gigabyte downloads, the pool keeps old versions
# so the URLs pinned in the PKGBUILD stay resolvable after the next release,
# and -- the point of doing it this way -- every checksum that reaches the
# PKGBUILD has been carried there under Anthropic's own signature:
#
# the bundled key, pinned by fingerprint -> signs InRelease
# InRelease -> hashes Packages
# Packages -> hashes each .deb
#
# A break anywhere in that chain fails the sync rather than proposing a
# checksum nobody vouched for.
set -euo pipefail

BASE_URL="https://downloads.claude.ai/claude-desktop/apt/stable"
declare -A DEB_ARCHES=([x86_64]=amd64 [aarch64]=arm64)

# "Anthropic Claude Code Release Signing <security@anthropic.com>", the
# trust anchor for everything below. 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. The key is committed next to this hook rather than
# fetched, so rotating it is a reviewed change to this package, not
# something the server can do to us.
KEY_FILE=".omarchy/anthropic-release-signing.key"
KEY_FPR='31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE'

MIN_AGE="${MIN_RELEASE_AGE_SECONDS:-0}"
BYPASS="${BYPASS_MIN_RELEASE_AGE:-}"

WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT

fail() {
echo "$*" >&2
exit 1
}

# --- trust anchor -----------------------------------------------------------
GNUPGHOME="$WORK_DIR/gnupg"
export GNUPGHOME
install -dm700 "$GNUPGHOME"

[[ -f "$KEY_FILE" ]] || fail "Signing key missing: $KEY_FILE"
gpg --batch --quiet --import "$KEY_FILE" 2>/dev/null ||
fail "Could not import $KEY_FILE"

got_fpr=$(gpg --batch --with-colons --fingerprint | awk -F: '/^fpr:/{print $10; exit}')
[[ "$got_fpr" == "$KEY_FPR" ]] ||
fail "Bundled key is not Anthropic's: got ${got_fpr:-none}, expected $KEY_FPR"

# --- the signed index -------------------------------------------------------
curl -fsSL -o "$WORK_DIR/InRelease" "$BASE_URL/dists/stable/InRelease" ||
fail "Could not fetch InRelease"

# Written out and read back rather than verified in place: gpg reports a good
# signature on a clearsigned file that carries unsigned text outside the armour.
gpg --batch --yes --output "$WORK_DIR/Release" --decrypt "$WORK_DIR/InRelease" >/dev/null 2>&1 ||
fail "InRelease is not signed by Anthropic's release key"

# Each architecture's Packages index, authenticated by the hash InRelease
# signs for it. Scoped to the SHA256 block: the same filenames recur under
# SHA512, and taking the first match regardless would compare a SHA256 sum
# against a SHA512 entry.
fetch_packages() {
local deb_arch="$1" path="main/binary-${1}/Packages"
local out="$WORK_DIR/Packages.$deb_arch" want got

want=$(awk -v path="$path" '
/^SHA256:/ { in_block = 1; next }
/^[A-Za-z][A-Za-z0-9-]*:/ { in_block = 0 }
in_block && $3 == path && length($1) == 64 { print $1; exit }
' "$WORK_DIR/Release")
[[ -n "$want" ]] || fail "InRelease carries no SHA256 for $path"

curl -fsSL -o "$out" "$BASE_URL/dists/stable/$path" ||
fail "Could not fetch $path"

got=$(sha256sum "$out" | cut -d' ' -f1)
[[ "$got" == "$want" ]] ||
fail "$path does not match the hash InRelease signs for it"

echo "$out"
}

# "<version> <sha256>" per stanza, for our package only. The pool is shared,
# and a stanza that carries no checksum must not donate its version to the
# next one, so both fields reset at each Package: head.
releases_in() {
awk '
{ sub(/\r$/, "") }
/^Package:/ { pkg = $2; version = sha256 = "" }
/^Version:/ { version = $2 }
/^SHA256:/ { sha256 = $2 }
/^$/ { if (pkg == "claude-desktop" && version && sha256) print version, sha256
pkg = version = sha256 = "" }
END { if (pkg == "claude-desktop" && version && sha256) print version, sha256 }
' "$1"
}

declare -A CHECKSUMS=() # "<arch> <version>" -> sha256
declare -A SEEN_COUNT=() # version -> number of arches offering it

for arch in "${!DEB_ARCHES[@]}"; do
deb_arch="${DEB_ARCHES[$arch]}"
packages=$(fetch_packages "$deb_arch")

while read -r version sha256; do
[[ -n "$version" ]] || continue
CHECKSUMS["$arch $version"]="$sha256"
SEEN_COUNT[$version]=$(( ${SEEN_COUNT[$version]:-0} + 1 ))
done < <(releases_in "$packages")
done

# A release lands one architecture at a time and a single pkgver covers both,
# so only versions present in every architecture are candidates.
candidates=()
for version in "${!SEEN_COUNT[@]}"; do
(( SEEN_COUNT[$version] == ${#DEB_ARCHES[@]} )) && candidates+=("$version")
done
(( ${#candidates[@]} )) || fail "No release found for every architecture in the signed index"

# Newest first, by pacman's comparator -- the one that decides whether a
# published package is an upgrade. sort -V disagrees with it at the corners.
newest_first=()
while (( ${#candidates[@]} )); do
best_index=0
for i in "${!candidates[@]}"; do
if [[ $(vercmp "${candidates[$i]}" "${candidates[$best_index]}") -gt 0 ]]; then
best_index=$i
fi
done
newest_first+=("${candidates[$best_index]}")
unset 'candidates[best_index]'
candidates=("${candidates[@]}")
done

# The pool serves a Last-Modified for each .deb, which is when that build was
# actually published. A release counts as published when its last architecture
# lands, so the newer of the two timestamps is the conservative one to age
# against.
published_at_of() {
local version="$1" newest_epoch=0 arch deb_arch header lm epoch

for arch in "${!DEB_ARCHES[@]}"; do
deb_arch="${DEB_ARCHES[$arch]}"
header=$(curl -fsSLI \
"$BASE_URL/pool/main/c/claude-desktop/claude-desktop_${version}_${deb_arch}.deb" 2>/dev/null) || return 1
lm=$(awk -F': ' 'tolower($1) == "last-modified" { sub(/\r$/, "", $2); print $2; exit }' <<<"$header")
[[ -n "$lm" ]] || return 1
epoch=$(date --date="$lm" +%s 2>/dev/null) || return 1
(( epoch > newest_epoch )) && newest_epoch=$epoch
done

(( newest_epoch > 0 )) || return 1
date -u --date="@$newest_epoch" +%Y-%m-%dT%H:%M:%SZ
}

# Walk newest to oldest and report the first release that has cleared the
# quarantine window, so a bad release held back does not also hold back the
# good one before it.
now=$(date +%s)
for version in "${newest_first[@]}"; do
published_at=$(published_at_of "$version") ||
fail "Could not establish a publication time for $version"

if (( MIN_AGE > 0 )) && [[ "$BYPASS" != "1" ]]; then
published_epoch=$(date --date="$published_at" +%s)
if (( now - published_epoch < MIN_AGE )); then
continue
fi
fi

jq -n \
--arg pkgver "$version" \
--arg published_at "$published_at" \
--arg x86_64 "${CHECKSUMS["x86_64 $version"]}" \
--arg aarch64 "${CHECKSUMS["aarch64 $version"]}" \
'{
pkgver: $pkgver,
published_at: $published_at,
sha256sums: { x86_64: [$x86_64], aarch64: [$aarch64] }
}'
exit 0
done

# Everything on offer is still inside the quarantine window.
echo '{}'
160 changes: 160 additions & 0 deletions pkgbuilds/claude-desktop/PKGBUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Maintainer: Arseniy Zarechnev <cyanidesign@gmail.com>
#
# The dependency set and the two Cowork path shims below were worked out by
# the AUR claude-desktop package (Kevin Diaz) and ya-claude (Aaron Bockelie);
# both were read closely while writing this. The signature-verifying upstream
# hook is new here.
#
# Anthropic publishes the desktop app only as a .deb, from their own apt
# repository; the docs point non-Debian distributions at the CLI instead.
# This package unpacks that .deb, which is the whole of it -- nothing is
# patched, added, or rebuilt.
#
# Like openai-codex-desktop, Omarchy tracks the vendor's own repository
# rather than the AUR: releases land several times a week.
# .omarchy/upstream.sh rewrites the version and checksums below from
# Anthropic's package index, after verifying that index's PGP signature.

pkgname=claude-desktop
pkgver=1.40609.1
pkgrel=1
pkgdesc="Official Claude desktop app: Chat, Cowork, and Claude Code"
arch=('x86_64' 'aarch64')
url="https://claude.com/download"
license=('LicenseRef-Proprietary')

# Two sources, reconciled: upstream's own "Depends:" field translated to Arch
# names (libgtk-3-0 -> gtk3, and so on), plus the libraries the shipped
# binaries actually link. namcap calls some of these unneeded -- those are
# reached through dlopen(), a spawned program, or D-Bus, none of which it can
# see in an ELF header. alsa-lib is only a "Recommends:" upstream, but
# Chromium needs it for audio.
depends=(
'alsa-lib'
'at-spi2-core'
'cairo'
'dbus'
'expat'
'gcc-libs'
'glib2'
'glibc'
'gtk3'
'hicolor-icon-theme'
'libcap-ng'
'libcups'
'libdrm'
'libnotify'
'libseccomp'
'libsecret'
'libx11'
'libxcb'
'libxcomposite'
'libxdamage'
'libxext'
'libxfixes'
'libxkbcommon'
'libxrandr'
'libxtst'
'mesa'
'nspr'
'nss'
'pango'
'socat'
'systemd-libs'
'util-linux-libs'
'virtiofsd'
'xdg-desktop-portal'
'xdg-utils'
)

# Cowork runs its agent inside a QEMU VM. Upstream ships the VM stack under
# "Recommends:", which apt installs by default, so depending on it here gives
# the same working-out-of-the-box Cowork that Debian and Ubuntu users get.
depends_x86_64=('qemu-system-x86' 'edk2-ovmf')
depends_aarch64=('qemu-system-aarch64' 'edk2-aarch64')

optdepends=(
'gnome-keyring: credential storage via Secret Service'
'kwallet: credential storage on KDE Plasma'
'libayatana-appindicator: system tray icon'
'xdg-desktop-portal-hyprland: portal backend on Hyprland'
)

makedepends=('libarchive')
# Other packagings of the same app, all of which own the same paths.
conflicts=('claude' 'claude-desktop-appimage' 'claude-desktop-bin'
'claude-desktop-extra' 'ya-claude')

# Prebuilt Electron: stripping corrupts the V8 snapshot and the embedded
# resources, and there is no source here to build a -debug package from.
options=('!strip' '!debug')

# Omarchy: same reasoning as openai-codex-desktop -- maximum zstd is worth the
# extra build minutes on a half-gigabyte Electron tree that every user
# re-downloads on each of Anthropic's frequent releases.
COMPRESSZST=(zstd -c -z -q --ultra -22 --threads=0 -)

_deb_x86_64="${pkgname}_${pkgver}_amd64.deb"
_deb_aarch64="${pkgname}_${pkgver}_arm64.deb"
_pool="https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop"
source=('claude-desktop-launcher.sh')
source_x86_64=("${_deb_x86_64}::${_pool}/${_deb_x86_64}")
source_aarch64=("${_deb_aarch64}::${_pool}/${_deb_aarch64}")
noextract=("${_deb_x86_64}" "${_deb_aarch64}")
sha256sums=('ce6de7bdfa99f6be6442ab2c16cf8890e8dce520d60e02d65ceaeb0c0c4dddfc')
sha256sums_x86_64=('80182e8511c6bbee6de26c7ee225fbd2a9aba2274ef1405a1d89cd8fe7a380dc')
sha256sums_aarch64=('493ccc06030c5dbb225add135823385d8cfb400a5922704de377ea8f55b39c40')

package() {
cd "${srcdir}"

local deb_var="_deb_${CARCH}"
local deb="${!deb_var}"

# Only the payload. The .deb's maintainer scripts register Anthropic's apt
# repository, install an AppArmor profile gated on Ubuntu's userns
# restriction, and register a GNOME Shell search provider -- none of which
# applies here, so none of it is reproduced.
bsdtar -xOf "${deb}" data.tar.xz |
bsdtar --no-same-owner -xf - -C "${pkgdir}"

# Chromium's setuid sandbox helper, for kernels without unprivileged user
# namespaces. The payload already carries this mode; setting it explicitly
# records that the setuid bit is deliberate.
chmod 4755 "${pkgdir}/usr/lib/claude-desktop/chrome-sandbox"

# /usr/bin/claude-desktop ships as a symlink to the Electron binary. Replace
# it with a launcher that names the safeStorage backend when a Secret
# Service is actually present -- without it, Electron's own detection falls
# through to plaintext on every non-GNOME, non-KDE session (so, on Omarchy),
# and the app refuses to persist a sign-in it cannot encrypt.
rm "${pkgdir}/usr/bin/claude-desktop"
install -Dm755 claude-desktop-launcher.sh "${pkgdir}/usr/bin/claude-desktop"

install -Dm644 "${pkgdir}/usr/share/doc/claude-desktop/copyright" \
"${pkgdir}/usr/share/licenses/${pkgname}/copyright"

# --- Cowork path shims ----------------------------------------------------
# The app looks for the VM stack where Debian puts it.
#
# virtiofsd: resolved from /usr/libexec then /usr/bin, with the bundled copy
# used only on Ubuntu 22.x, so on Arch the system binary is the one reached.
# Arch installs it in /usr/lib.
ln -s ../lib/virtiofsd "${pkgdir}/usr/bin/virtiofsd"

# UEFI firmware: on x86_64 the app opens /usr/share/OVMF/OVMF_CODE_4M.fd and
# derives the VARS path from it by substring replacement, so both links are
# needed. /usr/share/OVMF is Arch's compat symlink to /usr/share/edk2, where
# the firmware lives under x64/ with different names.
#
# aarch64 needs no shim: there the app opens /usr/share/AAVMF/AAVMF_CODE.fd,
# and edk2-aarch64 already installs it at exactly that path.
if [[ $CARCH == x86_64 ]]; then
install -d "${pkgdir}/usr/share/edk2"
ln -s x64/OVMF_CODE.4m.fd "${pkgdir}/usr/share/edk2/OVMF_CODE_4M.fd"
ln -s x64/OVMF_VARS.4m.fd "${pkgdir}/usr/share/edk2/OVMF_VARS_4M.fd"
fi

# Debian package-policy files are not used on Arch Linux.
rm -rf "${pkgdir}/usr/share/doc" "${pkgdir}/usr/share/lintian"
}
Loading