Deploy JFrog Boost with Jamf Pro MDM

Deploy JFrog Boost on managed macOS devices with Jamf Pro MDM.

This page explains how to use Jamf Pro assets to install Boost across managed macOS fleets and inventory the result. The scripts are stored under docs/customer-installation/mdm/jamf-pro, which render those files directly so edits in the repository update the website on the next build.

📘

Note

The deployment path described in this section was tested with Jamf Pro. Future mobile device management providers should follow the same source-file layout under docs/customer-installation/mdm/.

For individual developer setup, see JFrog Boost Quickstart.

This section covers three rollout tasks:

Add the Install Script

Create a Jamf Pro script from install.sh. The script installs Boost for the logged-in console user, initializes the selected agent integrations, and refreshes Jamf inventory.

To add the Boost install script to Jamf Pro:

  1. In Jamf Pro, go to Settings > Computer Management > Scripts and create a new script.
  2. Paste the install script that follows into the Jamf Pro script editor.
  3. Save the script for use in a deployment policy.
Installation script (install.sh)
  #!/bin/bash
  # Boost CLI - Jamf install (per-user, macOS).
  # Installs to $HOME/.local/bin/boost for the console user, runs `boost init`
  # as them, removes the legacy /usr/local/bin/boost if present, and refreshes
  # Jamf inventory so the Boost CLI Status EA reflects this run immediately.
  #
  # Optional:
  #   BOOST_TARGETS      default: "cursor claude"
  #   Jamf parameter $4  same as BOOST_TARGETS
  set -euo pipefail

  repo="jfrog/boost"
  targets="${4:-${BOOST_TARGETS:-cursor claude}}"

  log() { printf '[boost] %s\n' "$*"; }
  die() { log "ERROR: $*" >&2; exit 1; }

  # curl with retries — handles transient corporate-network resets (curl 56,
  # 7, 18, 35, …). --retry-all-errors covers mid-stream resets that plain
  # --retry won't catch.
  fetch() {
  curl -fsSL \
    --retry 5 --retry-delay 2 --retry-all-errors \
    --connect-timeout 10 --max-time 300 \
    "$@"
  }

  # Repair root-owned leftovers in the dirs `boost init` writes to. Earlier
  # POC runs may have created these as root, leaving the user unable to
  # overwrite them. Recursive chown is safe: if the dir itself is root-owned,
  # the user couldn't have created files inside it anyway.
  normalize_boost_paths() {
  local user="$1" home="$2" path owner
  for path in \
    "$home/.local" \
    "$home/.boost" \
    "$home/.claude" \
    "$home/.cursor" \
    "$home/.gemini" \
    "$home/.codex"; do
    [ -e "$path" ] || continue
    owner="$(/usr/bin/stat -f '%Su' "$path" 2>/dev/null || true)"
    if [ "$owner" != "$user" ]; then
      log "fixing ownership: $path (was $owner)"
      /usr/sbin/chown -R "$user" "$path" || log "chown failed for $path (non-fatal)"
    fi
  done
  }

  # ---- Console user is required for per-user install ----------------------
  user="$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)"
  if [ -z "$user" ] || [ "$user" = "root" ] || [ "$user" = "loginwindow" ]; then
  log "no console user; per-user install needs a logged-in account — skipping"
  # Exit 0 so Jamf doesn't flag this as a failure and storm retries.
  # Pair with a Login trigger on the policy to catch this machine later.
  exit 0
  fi
  uid="$(/usr/bin/id -u "$user")"
  home="$(/usr/bin/dscl . -read "/Users/$user" NFSHomeDirectory 2>/dev/null | /usr/bin/awk '{print $2}')"
  [ -n "$home" ] || home="/Users/$user"
  install_dir="$home/.local/bin"
  boost="$install_dir/boost"

  # ---- Architecture --------------------------------------------------------
  arch="$(uname -m)"
  case "$arch" in arm64|aarch64) arch=arm64 ;; x86_64|amd64) arch=amd64 ;; *) die "unsupported arch: $arch" ;; esac
  archive="boost-darwin-${arch}.tar.gz"

  # ---- Resolve latest tag and download ------------------------------------
  tag="$(fetch -I -o /dev/null -w '%{url_effective}' "https://github.com/$repo/releases/latest" | sed 's#.*/tag/##')"
  [ -n "$tag" ] || die "could not resolve latest Boost release"

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

  log "downloading $archive ($tag)"
  fetch "https://github.com/$repo/releases/download/$tag/$archive" -o "$tmp/$archive"
  tar -xzf "$tmp/$archive" -C "$tmp"
  [ -f "$tmp/boost" ] || die "archive missing boost binary"

  # ---- Install into the user's ~/.local/bin, owned by them ----------------
  /bin/mkdir -p "$install_dir"
  /usr/sbin/chown "$user" "$home/.local" "$install_dir" 2>/dev/null || true
  /usr/bin/install -o "$user" -g staff -m 0755 "$tmp/boost" "$boost"
  log "installed $(/usr/bin/sudo -H -u "$user" "$boost" version 2>/dev/null || echo "$tag") at $boost"

  # ---- Remove legacy system install so the EA reports the right path ------
  # The EA prefers $HOME/.local/bin/boost but falls back to /usr/local/bin/boost.
  # Leaving the old one around means PATH lookups for the user may still hit
  # the stale binary depending on shell config. Self-cleanup on every run.
  if [ -e /usr/local/bin/boost ]; then
  log "removing legacy /usr/local/bin/boost"
  /bin/rm -f /usr/local/bin/boost || log "legacy removal failed (non-fatal)"
  fi

  # ---- Fix any root-owned config leftovers before init --------------------
  normalize_boost_paths "$user" "$home"

  # ---- Run `boost init` as the console user -------------------------------
  flags=(--accept-terms)
  for target in $targets; do flags+=(--"$target"); done
  log "running '$boost init ${flags[*]}' as $user"
  /bin/launchctl asuser "$uid" /usr/bin/sudo -H -u "$user" \
  /usr/bin/env HOME="$home" USER="$user" LOGNAME="$user" \
  "$boost" init "${flags[@]}"

  # ---- Push Jamf inventory so the EA picks up this run --------------------
  if [ -x /usr/local/bin/jamf ]; then
  log "triggering jamf recon"
  /usr/local/bin/jamf recon >/dev/null 2>&1 || log "recon failed (non-fatal)"
  fi

Create a Deployment Policy

In Jamf Pro, add the installer as a script, then attach it to a policy scoped to the Macs that should receive Boost. Keep the policy idempotent: rerunning it refreshes the binary and re-applies the selected integrations.

SettingValue
ScriptPaste install.sh into a Jamf Pro script
Run asRoot. The script switches to the logged-in user for boost init.
Default install path$HOME/.local/bin/boost for the console user
Parameter 4Optional space-separated Boost targets, such as cursor claude

Target Examples

RolloutJamf parameter 4
Default local agent rolloutcursor claude
Cursor, Claude, and Geminicursor claude gemini
CLI-first teamscodex opencode

To create a Jamf Pro deployment policy for Boost:

  1. In Jamf Pro, create a policy scoped to the target Macs.
  2. Add the install script as a payload and set Run as to Root.
  3. (Optional) Set Jamf parameter 4 to the space-separated agent targets for your rollout.
  4. Add a Login trigger so Macs without an active console user receive Boost when a user logs in.
  5. Save and enable the policy.

Add the Extension Attribute

Add the extension attribute so Jamf Inventory reports whether Boost is installed, which version is present, where the binary was found, which integrations are active, and which AI tools were detected for the console user.

SettingValue
Display NameBoost CLI Status
InventoryApplications
Data TypeString
Input TypeScript

To add the Boost extension attribute to Jamf Pro:

  1. In Jamf Pro, open Settings > Computer Management > Extension Attributes and create a new attribute.
  2. Set the display name, inventory category, data type, and input type to match the extension attribute settings table.
  3. Paste the extension attribute script from the accordion into the script field.
  4. Save the extension attribute.
plusExtension attribute script (extension-attribute.sh)
  #!/bin/bash
  # Boost CLI — Jamf Pro Extension Attribute
  #
  # Looks for boost in this priority order:
  #   1. $home/.local/bin/boost   (per-user, current layout)
  #   2. /usr/local/bin/boost     (legacy fallback during migration)
  #   3. /Users/*/.local/bin/boost (last resort if no console user)
  #
  # IMPORTANT: EA values refresh ONLY when `jamf recon` runs. The Boost
  # install script must call `/usr/local/bin/jamf recon` at its end (or the
  # install policy must include a Maintenance → Update Inventory payload),
  # otherwise this attribute will keep showing the previously-recorded value.
  #
  # Deployment:
  #   Jamf Pro → Settings → Computer Management → Extension Attributes → New
  #     Display Name: Boost CLI Status
  #     Inventory:    Applications
  #     Data Type:    String
  #     Input Type:   Script

  set -u
  export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin"

  home=""
  boost=""
  boost_scope=""

  emit() { printf '%s=%s\n' "$1" "$2"; }

  resolve_home_for() {
    local u="$1" h
    h="$(/usr/bin/dscl . -read "/Users/$u" NFSHomeDirectory 2>/dev/null | /usr/bin/awk '{print $2}')"
    [ -z "$h" ] && h="/Users/$u"
    printf '%s' "$h"
  }

  have_cli() {
    local name="$1" p
    for p in \
      "$home/.local/bin/$name" \
      "/usr/local/bin/$name" \
      "/opt/homebrew/bin/$name" \
      "$home/.npm-global/bin/$name" \
      "$home/.bun/bin/$name" \
      "$home/.volta/bin/$name"; do
      [ -x "$p" ] && return 0
    done
    return 1
  }

  echo "<result>"

  # ---- EA metadata (most useful field for diagnosing stale inventory) ----
  emit ea_collected_at "$(/bin/date '+%Y-%m-%dT%H:%M:%S%z')"

  # ---- System --------------------------------------------------------------
  emit os       "$(/usr/bin/sw_vers -productName 2>/dev/null) $(/usr/bin/sw_vers -productVersion 2>/dev/null)"
  emit os_build "$(/usr/bin/sw_vers -buildVersion 2>/dev/null)"
  emit arch     "$(/usr/bin/uname -m 2>/dev/null)"

  # ---- Console user --------------------------------------------------------
  console_user="$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || echo "")"
  if [ -z "$console_user" ] || [ "$console_user" = "root" ] || [ "$console_user" = "loginwindow" ]; then
    emit console_user none
    console_user=""
  else
    emit console_user "$console_user"
    emit console_uid  "$(/usr/bin/id -u "$console_user")"
    home="$(resolve_home_for "$console_user")"
    emit home  "$home"
    shell="$(/usr/bin/dscl . -read "/Users/$console_user" UserShell 2>/dev/null | /usr/bin/awk '{print $2}')"
    emit shell "${shell:-unknown}"
  fi

  # ---- Locate boost (priority: console user → system → other users) -------
  if [ -n "$home" ] && [ -x "$home/.local/bin/boost" ]; then
    boost="$home/.local/bin/boost"
    boost_scope="user"
  elif [ -x "/usr/local/bin/boost" ]; then
    boost="/usr/local/bin/boost"
    boost_scope="system"
  else
    # No console user, or console user has nothing — scan other user homes
    # so overnight recon on an unattended Mac still finds boost.
    for udir in /Users/*/; do
      [ -d "$udir" ] || continue
      [ "${udir%/}" = "/Users/Shared" ] && continue
      [ "${udir%/}" = "$home" ]         && continue
      cand="${udir%/}/.local/bin/boost"
      if [ -x "$cand" ]; then
        boost="$cand"
        boost_scope="other_user"
        [ -z "$home" ] && home="${udir%/}"
        break
      fi
    done
  fi

  if [ -z "$boost" ]; then
    emit status not_installed
    echo "</result>"
    exit 0
  fi

  # ---- Boost binary report -------------------------------------------------
  emit status      installed
  emit boost_path  "$boost"
  emit boost_scope "$boost_scope"
  emit boost_mtime "$(/bin/date -r "$boost" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null)"
  emit boost_sha12 "$(/usr/bin/shasum -a 256 "$boost" 2>/dev/null | /usr/bin/awk '{print substr($1,1,12)}')"

  # Pass HOME so a per-user binary reads the right config dir, not /var/root.
  if [ -n "$home" ] && [ -d "$home" ]; then
    raw_version="$(/usr/bin/env HOME="$home" "$boost" version 2>/dev/null | /usr/bin/head -n1 | /usr/bin/tr -d '\r')"
  else
    raw_version="$("$boost" version 2>/dev/null | /usr/bin/head -n1 | /usr/bin/tr -d '\r')"
  fi
  parsed_version="$(printf '%s\n' "$raw_version" | /usr/bin/grep -Eo 'v?[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*' | /usr/bin/head -n1)"
  emit version     "${parsed_version:-unknown}"
  emit version_raw "${raw_version:-empty}"

  # ---- Integrations + AI tools (need a home dir) --------------------------
  if [ -n "$home" ] && [ -d "$home" ]; then
    integrations=()
    [ -f "$home/.cursor/hooks/boost-rewrite.sh" ] && integrations+=("cursor")
    [ -f "$home/.claude/hooks/boost-rewrite.sh" ] && integrations+=("claude")
    [ -f "$home/.gemini/hooks/boost-rewrite.sh" ] && integrations+=("gemini")
    [ -f "$home/.codex/BOOST.md" ]                && integrations+=("codex")
    if [ ${#integrations[@]} -eq 0 ]; then
      emit boost_integrations none
    else
      IFS=','; emit boost_integrations "${integrations[*]}"; unset IFS
    fi

    ai_tools=()
    [ -d "/Applications/Cursor.app" ] && ai_tools+=("cursor_app")
    [ -d "/Applications/Claude.app" ] && ai_tools+=("claude_app")
    have_cli claude   && ai_tools+=("claude_cli")
    have_cli codex    && ai_tools+=("codex_cli")
    have_cli gemini   && ai_tools+=("gemini_cli")
    have_cli opencode && ai_tools+=("opencode_cli")
    if [ ${#ai_tools[@]} -eq 0 ]; then
      emit ai_tools none
    else
      IFS=','; emit ai_tools "${ai_tools[*]}"; unset IFS
    fi

    if [ -f "$home/.boost/config.toml" ]; then
      emit config_global present
    else
      emit config_global absent
    fi

    last_init=""
    for ag in cursor claude gemini; do
      hook="$home/.$ag/hooks/boost-rewrite.sh"
      if [ -f "$hook" ]; then
        last_init="$(/bin/date -r "$hook" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null)"
        [ -n "$last_init" ] && break
      fi
    done
    [ -n "$last_init" ] && emit last_init "$last_init"
  fi

  echo "</result>"

Smart Group Examples

Use Smart Group criteria, such as the examples shown below, to quickly locate devices requiring Boost updates:

CriteriaPurpose
status=not_installedFinds Macs without the Boost binary.
version=v0.6.4Identifies machines still using an older Boost release.
ai_tools contains cursor plus
boost_integrations does not contain cursor
Finds Cursor users without Boost hooks.
📘

Notes

  • Boost is installed per user under $HOME/.local/bin, and therefore the installer requires a logged-in console user. If no console user is logged in, the script exits successfully without installing; use a Login trigger to catch the machine later.
  • The script removes any old system-wide versions of boost (/usr/local/bin/boost), if found, to prevent conflicts with the new per-user installation.
  • Before running boost init --accept-terms, the script repairs root-owned Boost and agent configuration directories left by earlier runs.
  • The script runs jamf recon when Jamf is available so the extension attribute reflects the latest install immediately.

Related Topics