Skip to content

Instantly share code, notes, and snippets.

@techjanitor
Created April 30, 2026 23:50
Show Gist options
  • Select an option

  • Save techjanitor/d15ae63ec9faf84d584641e57d83d1d8 to your computer and use it in GitHub Desktop.

Select an option

Save techjanitor/d15ae63ec9faf84d584641e57d83d1d8 to your computer and use it in GitHub Desktop.
Steam ARM64 native installer for Apple Silicon Macs (verifies Valve's code signature before installing)
#!/bin/bash
#
# Steam ARM64 Native Installer for Apple Silicon Macs
#
# Downloads the latest macOS Steam bootstrapper from Valve's CDN, verifies
# its Apple code signature, and installs it to /Applications. After install,
# Steam self-updates to the native ARM64 client (no Rosetta).
#
# Trust model:
# The integrity guarantee is the Apple code signature on Steam.app, which
# must be issued by Valve's developer Team ID (MXGJJ98X76). HTTPS to
# client-update.steamstatic.com and the SHA1 baked into the CDN filename
# are defense-in-depth: they catch corruption and accidental tampering,
# not adversarial substitution (SHA1 is collision-broken). The codesign
# check is the only thing that proves the bytes came from Valve.
#
# What it modifies:
# /Applications/Steam.app (replaced)
# /Applications/Steam.app.backup.<timestamp> (any prior install)
# ~/Library/Application Support/Steam/package/beta (set to publicbeta
# unless already set
# to a different value)
set -euo pipefail
readonly RED=$'\033[0;31m'
readonly GREEN=$'\033[0;32m'
readonly YELLOW=$'\033[1;33m'
readonly NC=$'\033[0m'
# Valve's Apple developer Team ID. The bootstrapper's code signature must
# match this; otherwise we refuse to install.
readonly EXPECTED_TEAM_ID="MXGJJ98X76"
# Set by main flow; consumed by cleanup() trap.
TMP_DIR=""
BACKUP=""
err() { printf '%sError: %s%s\n' "${RED}" "$*" "${NC}" >&2; }
warn() { printf '%sWarning: %s%s\n' "${YELLOW}" "$*" "${NC}"; }
info() { printf '%s%s%s\n' "${YELLOW}" "$*" "${NC}"; }
ok() { printf '%s%s%s\n' "${GREEN}" "$*" "${NC}"; }
cleanup() {
local exit_code=$?
if [[ -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then
rm -rf "${TMP_DIR}"
fi
# If the install failed after we moved the existing Steam.app aside but
# before a working replacement landed, restore the user's previous install.
if [[ ${exit_code} -ne 0 \
&& -n "${BACKUP}" \
&& -e "${BACKUP}" \
&& ! -e /Applications/Steam.app ]]; then
warn "Install did not complete; restoring previous install from ${BACKUP}"
mv "${BACKUP}" /Applications/Steam.app \
|| err "Could not restore backup from ${BACKUP}; manual recovery required."
fi
}
trap cleanup EXIT INT TERM
echo ""
echo "Steam ARM64 Native Installer for Apple Silicon Macs"
echo "===================================================="
echo ""
# --- Pre-flight checks ----------------------------------------------------
ARCH=$(uname -m)
if [[ "${ARCH}" != "arm64" ]]; then
err "This script is for Apple Silicon Macs only (detected: ${ARCH})."
exit 1
fi
if pgrep -x "steam_osx" > /dev/null || pgrep -x "Steam" > /dev/null; then
err "Steam is currently running. Please quit Steam fully and try again."
exit 1
fi
for cmd in curl unzip tar lipo codesign shasum xattr ditto; do
if ! command -v "${cmd}" >/dev/null 2>&1; then
err "required command not found: ${cmd}"
exit 1
fi
done
# --- Step 1: Fetch manifest -----------------------------------------------
info "Step 1/8: Fetching latest package info from Valve's CDN..."
MANIFEST=$(curl -sf "https://client-update.steamstatic.com/steam_client_osx") || {
err "Could not reach Valve's CDN. Check your internet connection."
exit 1
}
if [[ -z "${MANIFEST}" ]]; then
err "Manifest from CDN was empty."
exit 1
fi
# Pull the appdmg_osx block (skipping the steamchina mirror) and grab the
# first "file" entry from it.
APPDMG_BLOCK=$(printf '%s\n' "${MANIFEST}" | grep -A20 '"appdmg_osx"' | grep -v steamchina)
FILE=$(printf '%s\n' "${APPDMG_BLOCK}" | grep '"file"' | awk -F'"' '{print $4}' | head -n 1)
if [[ -z "${FILE}" ]]; then
err "Could not parse manifest filename. Valve may have changed CDN format."
exit 1
fi
# Steam's CDN uses content-addressable filenames: the trailing 40 hex chars
# are the SHA1 of the file. We use this to sanity-check the byte stream.
EXPECTED_SHA1=$(printf '%s\n' "${FILE}" | grep -oE '[a-f0-9]{40}$' || true)
echo " Bootstrapper: ${FILE}"
if [[ -z "${EXPECTED_SHA1}" ]]; then
err "Filename has no SHA1 suffix; CDN format has changed. Refusing to proceed."
err "Update this script to match Valve's new manifest layout."
exit 1
fi
echo " Expected SHA1: ${EXPECTED_SHA1}"
# --- Step 2: Download ------------------------------------------------------
info "Step 2/8: Downloading universal bootstrapper..."
TMP_DIR=$(mktemp -d)
curl -fL "https://client-update.steamstatic.com/${FILE}" -o "${TMP_DIR}/appdmg_osx.zip"
# --- Step 3: Verify download integrity -------------------------------------
info "Step 3/8: Verifying SHA1 of download against filename..."
ACTUAL_SHA1=$(shasum -a 1 "${TMP_DIR}/appdmg_osx.zip" | awk '{print $1}')
if [[ "${ACTUAL_SHA1}" != "${EXPECTED_SHA1}" ]]; then
err "SHA1 mismatch."
echo " Expected: ${EXPECTED_SHA1}"
echo " Actual: ${ACTUAL_SHA1}"
exit 1
fi
echo " Hash verified."
# --- Step 4: Extract -------------------------------------------------------
info "Step 4/8: Extracting..."
unzip -q "${TMP_DIR}/appdmg_osx.zip" -d "${TMP_DIR}"
tar xzf "${TMP_DIR}/SteamMacBootstrapper.tar.gz" -C "${TMP_DIR}"
if [[ ! -d "${TMP_DIR}/Steam.app" ]]; then
err "extracted archive does not contain Steam.app."
exit 1
fi
# --- Step 5: Verify arm64 slice -------------------------------------------
info "Step 5/8: Verifying ARM64 support..."
ARCHS=$(lipo -info "${TMP_DIR}/Steam.app/Contents/MacOS/steam_osx" 2>&1)
if ! printf '%s\n' "${ARCHS}" | grep -q "arm64"; then
err "downloaded binary does not contain arm64 slice. Aborting."
exit 1
fi
echo " ${ARCHS}"
# --- Step 6: Verify Valve code signature on staged copy -------------------
info "Step 6/8: Verifying Valve code signature (pre-install)..."
if ! codesign --verify --strict --deep "${TMP_DIR}/Steam.app" 2>/dev/null; then
err "code signature is invalid or has been tampered with."
exit 1
fi
TEAM=$(codesign -dv "${TMP_DIR}/Steam.app" 2>&1 | awk -F= '/TeamIdentifier/ {print $2}')
if [[ "${TEAM}" != "${EXPECTED_TEAM_ID}" ]]; then
err "signature is not from Valve."
echo " Expected Team ID: ${EXPECTED_TEAM_ID}"
echo " Got Team ID: ${TEAM:-<none>}"
exit 1
fi
ok " Signature valid. Signed by Valve (Team ID: ${TEAM})."
# Strip quarantine on the staged copy now, so the bundle that lands in
# /Applications is already clean. The signature was just verified above,
# so there is nothing to gate on first launch.
xattr -dr com.apple.quarantine "${TMP_DIR}/Steam.app" 2>/dev/null || true
# --- Step 7: Install -------------------------------------------------------
info "Step 7/8: Installing to /Applications..."
if [[ -e /Applications/Steam.app ]]; then
BACKUP="/Applications/Steam.app.backup.$(date +%Y%m%d-%H%M%S)"
echo " Existing Steam.app found; moving to ${BACKUP}"
mv /Applications/Steam.app "${BACKUP}"
fi
# ditto preserves all xattrs/ACLs/flags, so the codesign seal stays intact
# across the copy. cp -R does not.
ditto "${TMP_DIR}/Steam.app" /Applications/Steam.app
# --- Step 8: Re-verify the installed bundle -------------------------------
info "Step 8/8: Re-verifying signature on installed bundle..."
if ! codesign --verify --strict --deep /Applications/Steam.app 2>/dev/null; then
err "Post-install signature check failed. The installed copy may be corrupt."
err "Removing the broken install; previous version will be restored."
rm -rf /Applications/Steam.app
exit 1
fi
ok " Installed bundle verified."
# Beta channel — required for the native ARM64 client today. Don't trample
# a deliberately-different value the user has set.
STEAM_PKG_DIR="${HOME}/Library/Application Support/Steam/package"
mkdir -p "${STEAM_PKG_DIR}"
BETA_FILE="${STEAM_PKG_DIR}/beta"
CURRENT_BETA=""
if [[ -s "${BETA_FILE}" ]]; then
CURRENT_BETA=$(<"${BETA_FILE}")
fi
if [[ -z "${CURRENT_BETA}" || "${CURRENT_BETA}" == "publicbeta" ]]; then
echo "publicbeta" > "${BETA_FILE}"
echo " Beta channel set to: publicbeta"
else
warn "Existing beta channel '${CURRENT_BETA}' preserved."
warn "If you want the native ARM64 client, set ${BETA_FILE} to 'publicbeta'."
fi
echo ""
ok "Done! Steam is now installed as a universal binary."
echo ""
echo "What happens next:"
echo " 1. Launch Steam from /Applications"
echo " 2. Steam will self-update to the full native ARM64 client"
echo " 3. You will NOT be prompted to install Rosetta"
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment