Created
June 29, 2026 13:29
-
-
Save hongkongkiwi/958054c98672b85ddcab6cd136213a48 to your computer and use it in GitHub Desktop.
Ad-hoc re-codesign macOS .app bundles with optional xattr clearing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| usage() { | |
| cat <<EOF | |
| Usage: $(basename "$0") [--clear-attrs] <path-to-app> | |
| Re-codesign an .app bundle deeply with an ad-hoc signature. | |
| Options: | |
| --clear-attrs, -q Recursively clear ALL extended attributes on the bundle | |
| (com.apple.quarantine, com.apple.provenance, com.apple.macl, | |
| etc.) before re-signing. Useful for downloaded apps that | |
| refuse to launch. Clears recursively via xattr -cr. | |
| Argument forms accepted: | |
| /Applications/Stremio.app full path | |
| /Applications/Stremio full path without extension | |
| Stremio bare name -> /Applications/Stremio.app | |
| Stremio.app bare name with extension -> /Applications/Stremio.app | |
| Examples: | |
| $(basename "$0") Stremio | |
| $(basename "$0") /Applications/My App.app | |
| $(basename "$0") -q Stremio | |
| EOF | |
| } | |
| # Strip a trailing ".app" from the argument if present. | |
| strip_app_suffix() { | |
| local arg="$1" | |
| if [[ "$arg" == *.app ]]; then | |
| printf '%s' "${arg%.app}" | |
| else | |
| printf '%s' "$arg" | |
| fi | |
| } | |
| # Resolve argument: accept full path, full path without extension, | |
| # bare name, or bare name with extension. Resolves to /Applications/<name>.app | |
| # only when the argument isn't an existing directory path. | |
| # | |
| # Returns 0 and prints resolved path on success; returns 1 and prints error | |
| # to stderr on failure. Caller MUST check exit status — under set -e in | |
| # bash 3.2, the failing command substitution in an assignment does not abort. | |
| resolve_target() { | |
| local arg="$1" | |
| # Reject empty input immediately — produces a clean error instead of | |
| # the nonsensical "checked /Applications/.app". | |
| if [[ -z "$arg" ]]; then | |
| echo "ERROR: Empty argument. Pass an .app bundle path or name." >&2 | |
| return 1 | |
| fi | |
| # Full path to an existing directory (e.g. /Applications/Stremio.app, | |
| # /Volumes/App/Stremio.app, ~/Apps/My App.app). | |
| if [[ -d "$arg" ]]; then | |
| printf '%s' "$arg" | |
| return | |
| fi | |
| # Full path without extension (e.g. /Applications/Stremio). | |
| if [[ -d "${arg}.app" ]]; then | |
| printf '%s' "${arg}.app" | |
| return | |
| fi | |
| # Bare name, optionally with .app suffix (e.g. Stremio, Stremio.app). | |
| local name | |
| name="$(strip_app_suffix "$arg")" | |
| if [[ -d "/Applications/${name}.app" ]]; then | |
| printf '%s' "/Applications/${name}.app" | |
| return | |
| fi | |
| # Resolution failed — tailor the error to the form the user passed. | |
| if [[ "$arg" == /* ]]; then | |
| # Absolute path that doesn't exist — don't invent a /Applications fallback. | |
| echo "ERROR: '$arg' is not an existing .app bundle." >&2 | |
| else | |
| echo "ERROR: No .app bundle found for '$arg' (checked /Applications/${name}.app)." >&2 | |
| fi | |
| return 1 | |
| } | |
| clear_attrs=0 | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -h|--help) usage; exit 0 ;; | |
| -q|--clear-attrs) clear_attrs=1; shift ;; | |
| --) shift; break ;; | |
| -*) echo "Unknown option: $1" >&2; usage; exit 2 ;; | |
| *) break ;; | |
| esac | |
| done | |
| if [[ $# -ne 1 ]]; then | |
| usage | |
| exit 2 | |
| fi | |
| # Preflight: codesign is the core tool. Fail fast with a clear message if missing. | |
| if ! command -v codesign >/dev/null 2>&1; then | |
| echo "ERROR: 'codesign' not found on PATH. Install Xcode Command Line Tools: xcode-select --install" >&2 | |
| exit 1 | |
| fi | |
| # Resolve target. resolve_target prints errors to stderr and returns 1 on | |
| # failure — check explicitly to avoid the doubled-error bug where bash 3.2's | |
| # set -e leaves $target empty and the downstream .app suffix check fires. | |
| if ! target="$(resolve_target "$1")"; then | |
| exit 1 | |
| fi | |
| # Enforce that the resolved target is actually an .app bundle. The script is | |
| # named codesign-app; signing arbitrary directories or non-.app bundles is out | |
| # of scope and almost always a user mistake. | |
| if [[ "$target" != *.app ]]; then | |
| echo "ERROR: '$target' is not an .app bundle (must end in .app)." >&2 | |
| echo " To sign other bundle types, call codesign directly." >&2 | |
| exit 1 | |
| fi | |
| # Optional: clear extended attributes recursively (quarantine, provenance, macl, | |
| # etc.). Common requirement for downloaded apps that Gatekeeper blocks. | |
| # Non-fatal: if xattr fails (SIP, permissions, missing binary), warn with the | |
| # real error message and continue to the re-sign. | |
| if [[ $clear_attrs -eq 1 ]]; then | |
| if ! command -v xattr >/dev/null 2>&1; then | |
| echo "WARN: 'xattr' not found on PATH; skipping attribute clear." >&2 | |
| else | |
| echo "Clearing extended attributes on: $target" | |
| local_xattr_err="" | |
| if ! local_xattr_err="$(xattr -cr "$target" 2>&1)"; then | |
| echo "WARN: xattr -cr failed. Continuing with re-sign." >&2 | |
| if [[ -n "$local_xattr_err" ]]; then | |
| printf '%s\n' "$local_xattr_err" | sed 's/^/ xattr: /' >&2 | |
| fi | |
| fi | |
| fi | |
| fi | |
| echo "Re-signing: $target" | |
| # NOTE: --deep is deprecated for Developer ID signing but remains the correct | |
| # tool for ad-hoc (-) re-signing of bundles with nested code (helpers, frameworks). | |
| sudo codesign --force --deep --sign - "$target" | |
| echo | |
| echo "Verifying signature:" | |
| # Informational only — codesign -dv may report issues on nested code even when | |
| # the top-level sign succeeded. Do not let it abort the script. | |
| codesign -dv --verbose=4 "$target" 2>&1 | sed 's/^/ /' || true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment