Skip to content

Instantly share code, notes, and snippets.

@amoilanen
Last active July 5, 2026 14:04
Show Gist options
  • Select an option

  • Save amoilanen/1adf35d894ab73eecbacb6dc0870648f to your computer and use it in GitHub Desktop.

Select an option

Save amoilanen/1adf35d894ab73eecbacb6dc0870648f to your computer and use it in GitHub Desktop.
Install Claude Desktop and Zenflow on an Arch based Linux
#!/usr/bin/env bash
#
# Install the latest Claude Desktop on any Arch Linux derivative
# (Arch, Manjaro, EndeavourOS, CachyOS, Garuda, ...).
#
# Anthropic only ships Claude Desktop as a Debian .deb (amd64). Rather than
# rely on dpkg, we resolve the newest package from Anthropic's apt repository,
# verify its SHA256, extract it, and copy the /opt and /usr payload into place.
set -euo pipefail
REPO_BASE="https://downloads.claude.ai/claude-desktop/apt/stable"
PACKAGES_URL="${REPO_BASE}/dists/stable/main/binary-amd64/Packages"
# Work relative to this script's location, not the current working directory.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
DATE_SUFFIX="$(date +%d_%m_%Y)"
DEB_FILE="ClaudeDesktop.deb"
# --- helpers ---------------------------------------------------------------
have() { command -v "$1" >/dev/null 2>&1; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
# Run a command as root, using sudo (or doas) only when we are not already root.
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif have sudo; then
sudo "$@"
elif have doas; then
doas "$@"
else
die "need root privileges but neither sudo nor doas is available"
fi
}
# Fetch a URL. With a second arg, save to that file; otherwise stream to stdout.
fetch() {
local url="$1" out="${2:-}"
if have curl; then
if [ -n "$out" ]; then curl -fL -o "$out" "$url"; else curl -fsSL "$url"; fi
elif have wget; then
if [ -n "$out" ]; then wget -O "$out" "$url"; else wget -qO- "$url"; fi
else
die "need a downloader: install 'curl' or 'wget'"
fi
}
# Unpack a .deb (an 'ar' archive whose data.tar.* holds the payload) into $2.
extract_deb() {
local deb="$1" dest="$2"
if have bsdtar; then
# Layer 1: the .deb 'ar' container -> yields data.tar.*
# Layer 2: the data tarball -> yields the opt/ and usr/ trees.
# bsdtar (libarchive) handles the ar container and every compression
# (gz/xz/zst) without needing binutils' 'ar' or a matching GNU tar.
bsdtar -xf "$deb" -C "$dest"
bsdtar -xf "$dest"/data.tar.* -C "$dest"
elif have ar; then
( cd "$dest" && ar x "$deb" && tar -xf data.tar.* )
else
die "no .deb extractor found: install 'libarchive' (bsdtar) or 'binutils' (ar)"
fi
}
# --- preflight -------------------------------------------------------------
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) ;;
*) die "Claude Desktop is only published for x86_64 (amd64); this system is $ARCH" ;;
esac
have sha256sum || die "need 'sha256sum' (coreutils) to verify the download"
# --- resolve latest version ------------------------------------------------
echo "==> Resolving latest Claude Desktop version..."
read -r VERSION FILENAME SHA256 < <(
fetch "$PACKAGES_URL" | awk '
/^Version:/ { v = $2 }
/^Filename:/ { f = $2 }
/^SHA256:/ { s = $2 }
/^$/ { if (v != "") print v "\t" f "\t" s; v = f = s = "" }
END { if (v != "") print v "\t" f "\t" s }
' | sort -V | tail -1
)
[ -n "${FILENAME:-}" ] || die "could not determine latest package from $PACKAGES_URL"
echo " Latest version: $VERSION"
# --- download --------------------------------------------------------------
DEB_URL="${REPO_BASE}/${FILENAME}"
echo "==> Downloading $DEB_URL"
fetch "$DEB_URL" "$DEB_FILE"
cp "$DEB_FILE" "ClaudeDesktop_${VERSION}_${DATE_SUFFIX}.deb"
echo "==> Verifying SHA256 checksum..."
echo "${SHA256} ${DEB_FILE}" | sha256sum -c - || die "checksum verification failed"
# --- extract ---------------------------------------------------------------
# Unpack into a temporary staging directory so we never depend on / pollute
# the working directory's layout.
echo "==> Extracting package..."
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/claude-desktop.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
extract_deb "$SCRIPT_DIR/$DEB_FILE" "$STAGE"
# --- install ---------------------------------------------------------------
# Copy whatever top-level system trees the package provides (typically opt/ and
# usr/) into the filesystem root.
echo "==> Installing to /opt and /usr (requires root)..."
installed=0
for tree in opt usr; do
if [ -d "$STAGE/$tree" ]; then
as_root cp -a "$STAGE/$tree/." "/$tree/"
installed=1
fi
done
[ "$installed" -eq 1 ] || die "extracted package contained no opt/ or usr/ tree"
echo "==> Updating desktop/icon caches..."
have update-desktop-database && as_root update-desktop-database /usr/share/applications 2>/dev/null || true
have gtk-update-icon-cache && as_root gtk-update-icon-cache -f /usr/share/icons/hicolor 2>/dev/null || true
echo "==> Done. Claude Desktop $VERSION installed. Launch with: claude-desktop"
#!/usr/bin/env bash
#
# Install Zenflow on any Arch Linux derivative (Arch, Manjaro, EndeavourOS, ...).
#
# The upstream package ships as an .rpm. We do not rely on any RPM-specific
# tooling being present: extraction is done with bsdtar, which is provided by
# libarchive, a mandatory dependency of pacman and therefore guaranteed to
# exist on every Arch-based system. Fallbacks are provided just in case.
set -euo pipefail
URL="https://download.zencoder.ai/zenflowapp/latest/linux-x64/Zenflow.rpm"
# Work relative to this script's location, not the current working directory.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
DATE_SUFFIX="$(date +%d_%m_%Y)"
RPM="Zenflow.rpm"
# --- helpers ---------------------------------------------------------------
have() { command -v "$1" >/dev/null 2>&1; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
# Run a command as root, using sudo (or doas) only when we are not already root.
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif have sudo; then
sudo "$@"
elif have doas; then
doas "$@"
else
die "need root privileges but neither sudo nor doas is available"
fi
}
# --- download --------------------------------------------------------------
if have curl; then
curl -fL -o "$RPM" "$URL"
elif have wget; then
wget -O "$RPM" "$URL"
else
die "need a downloader: install 'curl' or 'wget'"
fi
cp "$RPM" "Zenflow_${DATE_SUFFIX}.rpm"
# --- extract ---------------------------------------------------------------
# Unpack into a temporary staging directory so we never depend on / pollute
# the working directory's layout.
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/zenflow.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
if have bsdtar; then
# bsdtar (libarchive) reads .rpm directly and is always present on Arch.
bsdtar -xf "$SCRIPT_DIR/$RPM" -C "$STAGE"
elif have rpm2cpio && have cpio; then
( cd "$STAGE" && rpm2cpio "$SCRIPT_DIR/$RPM" | cpio -idmv )
elif have rpmextract.sh; then
( cd "$STAGE" && rpmextract.sh "$SCRIPT_DIR/$RPM" )
else
die "no RPM extractor found: install 'libarchive' (bsdtar) or 'rpmextract'"
fi
# --- install ---------------------------------------------------------------
# Copy whatever top-level system trees the package provides (typically opt/ and
# usr/) into the filesystem root.
installed=0
for tree in opt usr; do
if [ -d "$STAGE/$tree" ]; then
as_root cp -a "$STAGE/$tree/." "/$tree/"
installed=1
fi
done
[ "$installed" -eq 1 ] || die "extracted package contained no opt/ or usr/ tree"
echo "Zenflow installed successfully."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment