> ## Documentation Index
> Fetch the complete documentation index at: https://docs.asymptotelabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Beacon with Rippling and S3

> Roll out Beacon to Apple Silicon Macs with Rippling and send runtime and inventory telemetry to your own S3 bucket.

This guide takes you from no Beacon on your Macs to every Mac in scope sending its AI agent activity to an S3 bucket you own. Follow the steps in order. Everything is done in Rippling; nobody installs anything by hand.

## What you'll set up

You add three items to Rippling and scope all three to the same Supergroup:

| Rippling item | What it does on each Mac |
| - | - |
| A custom profile | Approves Beacon's background services, so users get no "Background Items Added" prompt and can't turn them off. |
| The Beacon package, as custom software | Installs Beacon under `/opt/beacon` and starts its collector. If someone is logged in, it also configures their Claude Code and Codex. |
| A daily script | Sets up the S3 forwarder and configures the logged-in user's Claude Code, Codex and Cursor. Re-checks every day and fixes anything missing. |

When you finish, each Mac uploads two streams to your bucket:

```text theme={null}
s3://<bucket>/<prefix>/runtime/date=YYYY-MM-DD/<time>-<id>.jsonl.gz
s3://<bucket>/<prefix>/inventory/date=YYYY-MM-DD/<time>-<id>.jsonl.gz
```

`runtime` holds agent activity. `inventory` holds a snapshot of each Mac's agents, taken every 6 hours.

## Before you start

| You need | Why |
| - | - |
| Rippling IT with Device Management | Custom software, custom profiles and scripts are in the Devices app. |
| Macs enrolled in Rippling MDM, each assigned to an employee | Rippling only installs software on assigned Macs. |
| Apple Silicon Macs | The Beacon package is Apple Silicon only. Leave Intel Macs out of the Supergroup. |
| A pilot Supergroup with two or three people in it | You test on them before everyone else. |
| An AWS account where you can create an S3 bucket and an IAM user | Step 2. |
| A Mac to upload from | Rippling only accepts custom profiles uploaded from a Mac. |

## 1. Try Beacon on one Mac (optional)

To see what Beacon does before involving Rippling, follow [Try Beacon on One Mac](/mdm/try-on-one-mac). It installs the same package by hand and sends nothing off the Mac.

## 2. Prepare S3

If a Mac already sends Beacon telemetry to S3, you can use the same bucket, prefix and access key and skip to step 3.

Run these commands on your own computer with the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html), signed in to the AWS account that will hold the bucket.

**Check which AWS account you're signed in to:**

```bash theme={null}
aws sts get-caller-identity
```

It prints the account ID and your identity. Make sure it's the right account.

**Set your values.** Pick a prefix such as `beacon-prod`. Beacon adds `runtime/` and `inventory/` under it, so don't include either.

```bash theme={null}
export AWS_REGION="us-west-2"
export BEACON_S3_BUCKET="<bucket>"
export BEACON_S3_PREFIX="beacon-prod"
```

**Create the bucket**, if you don't have one yet:

```bash theme={null}
if [ "$AWS_REGION" = "us-east-1" ]; then
  aws s3api create-bucket --bucket "$BEACON_S3_BUCKET" --region "$AWS_REGION"
else
  aws s3api create-bucket --bucket "$BEACON_S3_BUCKET" --region "$AWS_REGION" \
    --create-bucket-configuration "LocationConstraint=$AWS_REGION"
fi
```

It prints the bucket's location. AWS needs a different command for `us-east-1`, which is why there are two.

**Create an IAM user that can only add objects** under the prefix:

```bash theme={null}
aws iam create-user --user-name beacon-endpoint-writer

aws iam put-user-policy \
  --user-name beacon-endpoint-writer \
  --policy-name BeaconEndpointWriteOnly \
  --policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [{
      \"Effect\": \"Allow\",
      \"Action\": [\"s3:PutObject\"],
      \"Resource\": \"arn:aws:s3:::${BEACON_S3_BUCKET}/${BEACON_S3_PREFIX}/*\"
    }]
  }"
```

The first command prints the new user. The second prints nothing when it works.

This key can add objects under the prefix and nothing else: it can't list, read or delete them. Because it can't list the bucket, the forwarder logs one health-check error each time it starts. Uploads still work.

**Create an access key** for the user:

```bash theme={null}
aws iam create-access-key --user-name beacon-endpoint-writer
```

It prints an `AccessKeyId` (starting with `AKIA`) and a `SecretAccessKey`. Save both in your password manager now; AWS shows the secret only once. You paste them into the Rippling script in step 5.

**Test the key** by uploading a small file with it. Store it as a named profile first, so the secret isn't typed on a command line:

```bash theme={null}
aws configure --profile beacon-writer
```

Enter the access key ID, secret access key and region when asked, and press Enter for the output format. Then upload a test file:

```bash theme={null}
echo "Beacon key check" | aws s3 cp - "s3://${BEACON_S3_BUCKET}/${BEACON_S3_PREFIX}/key-check.txt" \
  --profile beacon-writer --region "$AWS_REGION"
```

It prints `upload: - to s3://<bucket>/<prefix>/key-check.txt`. A new key can take a minute to start working; if you get `InvalidAccessKeyId`, wait and try again.

Then delete the test file with your own identity:

```bash theme={null}
aws s3 rm "s3://${BEACON_S3_BUCKET}/${BEACON_S3_PREFIX}/key-check.txt" --region "$AWS_REGION"
```

Finally, delete the `[beacon-writer]` section from `~/.aws/credentials` and the `[profile beacon-writer]` section from `~/.aws/config`, so the key stays only in your password manager.

Two things about the bucket:

* If its default encryption uses a customer managed KMS key, also allow `kms:GenerateDataKey` on that key in the user's policy.
* Beacon sends no encryption headers. A bucket policy that requires one on every upload rejects Beacon's uploads.

## 3. Add the background-items profile

1. On a Mac, save the profile in [Reference: profile](#reference-profile) as `beacon-background-items.mobileconfig`.
2. In Rippling, go to **IT > Devices > Policies > Library > macOS library** and click **Upload**.
3. Name it **Beacon background services**, choose **macOS**, select the file, and click **Save & continue**.
4. Deploy it to the pilot Supergroup.

Do this before step 4, so the profile is on the Macs before the package installs. Macs pick it up within about 10 minutes.

## 4. Add the Beacon package

**Download and verify the package** on your Mac:

```bash theme={null}
VERSION="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/asymptote-labs/agent-beacon/releases/latest)"; VERSION="${VERSION##*/v}"
PKG="BeaconEndpointAgent-${VERSION}-arm64.pkg"
BASE="https://github.com/asymptote-labs/agent-beacon/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/${PKG}" && curl -fsSLO "${BASE}/${PKG}.sha256"
test "$(awk '{print $1}' "${PKG}.sha256")" = "$(shasum -a 256 "${PKG}" | awk '{print $1}')" \
  && pkgutil --check-signature "${PKG}"
```

`pkgutil` should print `Developer ID Installer: Asymptote Labs, Inc. (3A98D35XJR)`.

**Upload it.** Go to **IT > Devices > Software > Software libraries**, filter to **Custom software**, and click **Upload > Upload Software**:

| Field | Value |
| - | - |
| Name | Beacon Endpoint Agent |
| Operating system | macOS |
| Installer | The `.pkg` you downloaded |
| Installation check, pre-install and post-install scripts | Leave all three empty |

Leave the script fields empty. Rippling then tracks the package by its version, so new versions upgrade normally.

The upload shows **Queued**, then **Ready**, usually within 10 minutes. If it shows **Error**, delete it and upload it again.

**Deploy it** to the pilot Supergroup: **Deploy > Save and deploy**.

## 5. Add the configure script

1. Copy the script in [Reference: configure script](#reference-configure-script).
2. Fill in the five settings at the top: bucket, region, prefix, access key ID and secret access key.
3. In Rippling, go to **IT > Devices > Scripts > Add Script** and paste it in.
4. Use these settings:

| Setting | Value |
| - | - |
| Name | Beacon: configure |
| Platform | macOS |
| Supergroup | The pilot Supergroup |
| Frequency | Daily |
| Run on newly enrolled devices | Yes |
| Run this script now | Yes |

Each run prints one line per part, for example:

```text theme={null}
OK: S3 forwarder configured and running
OK: claude,codex configured for alice
OK: collector running
INFO: beacon version 1.3.33 (...)
INFO: last local event 42s ago
```

A line starting with `WAIT` is normal. It means Beacon isn't installed yet or nobody is logged in, and the next run finishes the job. To skip the wait on a Mac, open it in Rippling and click **Run script** on its **Activity** tab.

<Warning>
  Rippling admins who can open this script can read the access key. Limit Devices admin rights to the people who should have it. On the Mac, the script stores the key in a file only root can read, and never prints it.
</Warning>

## 6. Check a pilot Mac

**In Rippling**, the pilot Mac shows the profile installed, **Beacon Endpoint Agent** installed, and the script's latest run with only `OK` and `INFO` lines.

**On the Mac**, run:

```bash theme={null}
sudo /opt/beacon/bin/beacon endpoint status --system
```

`Service: loaded=true running=true` and `telemetry=enabled` next to Claude Code and Codex mean it's working.

**In S3**, write a test event on the Mac, wait five minutes (uploads go in batches), then list today's objects:

```bash theme={null}
sudo /opt/beacon/bin/beacon endpoint s3 validate --system
```

```bash theme={null}
aws s3 ls "s3://<bucket>/<prefix>/runtime/date=$(date -u +%F)/" --region <region>
```

Then quit and reopen Claude Code, run one small task, and check that a newer object arrives.

**Check the key isn't readable by users.** On the Mac, logged in as a normal user, run this with your access key ID. It should print nothing:

```bash theme={null}
grep -rls "<access key id>" /Library /Users/Shared /private/tmp /private/var/tmp /usr/local 2>/dev/null
```

<Note>
  Get the key, region and bucket policy right on the pilot. If S3 rejects an upload, the forwarder retries for about five minutes and then drops it.
</Note>

## 7. Roll out to everyone

Scope all three items to your full Supergroup, in this order:

1. The **Beacon background services** profile.
2. **Beacon Endpoint Agent**.
3. The **Beacon: configure** script. Saving it runs it on every Mac in scope.

Keep all three on the same Supergroup. Track progress in **Software > My Software** (installed, pending and failed counts) and on the script's page (each Mac's latest output). Macs that are offline catch up when they come back online.

## Update Beacon

1. Download and verify the new package as in step 4.
2. Open **Beacon Endpoint Agent** in the software library and choose **Manage > New version**.
3. Upload the new package, with the script fields still empty.

The package keeps the S3 forwarder's settings, and the next script run updates anything that changed. Watch two or three Macs after each upgrade.

## Rotate the S3 key

1. Create a second access key for the Beacon IAM user.
2. Put the new key ID and secret in the **Beacon: configure** script and save it. Rippling runs it on every Mac in scope.
3. Check that new objects still arrive in S3.
4. Wait until every Mac has run the updated script, then deactivate and delete the old key. A Mac still using a deactivated key can't upload until it switches.

## Remove Beacon

1. Take the Mac, or the employee, out of the Beacon Supergroup. Rippling reinstalls software it still targets within an hour.
2. Add the script in [Reference: remove script](#reference-remove-script) as **Beacon: remove**, frequency **Once**, and run it on the Mac.
3. Its output ends with `OK: Beacon removed` when everything is gone: Beacon's services, files, logs, package receipt, S3 key file, and its hooks in each user's Claude Code, Codex and Cursor.

Don't use Rippling's **Uninstall** action for Beacon. It removes the package's files and may leave Beacon's background services running.

To remove Beacon everywhere, do the same for the whole Supergroup, then deactivate the access key in AWS.

## Troubleshooting

| What you see | What to do |
| - | - |
| Beacon stays **Pending** on a Mac | Check the Mac is assigned to an employee and online. Then click **... > MDM and Agent > Force MDM check-in** on the device. |
| Beacon shows **Error** on a Mac | Read the error in **My Software**, fix it, then click **Retry failed deployments**. Intel Macs fail here; take them out of the Supergroup. |
| `WAIT: Beacon is not installed yet` or `WAIT: a package install is running` | Normal. The next run finishes. Click **Run script** on the device to finish now. |
| `WAIT: nobody is logged in` | Normal. The next run after someone logs in configures their tools. |
| `FAIL: S3 forwarder setup failed` with `AccessDenied` | The IAM policy doesn't allow `s3:PutObject` under that bucket and prefix, or the bucket requires an encryption header. With a KMS-encrypted bucket, also allow `kms:GenerateDataKey`. |
| `FAIL` with `InvalidAccessKeyId` or `SignatureDoesNotMatch` | The key ID or secret in the script is wrong or deactivated. Fix them and save the script. |
| Users saw a "Background Items Added" notification | The package installed before the profile. It's harmless; check the profile is installed on that Mac. |
| No Claude Code activity from a Mac | Quit and reopen Claude Code. If that doesn't help, check whether your Claude admin settings turn on `allowManagedHooksOnly`, which blocks Beacon's hooks. |
| `FAIL: Beacon hooks remain for: ...` after removal | Put the Mac back in the Supergroup, let Beacon reinstall, then run **Beacon: remove** again. |

For anything else, run `sudo /opt/beacon/bin/beacon endpoint status --system` on the Mac and send the output to Asymptote support. The script's detailed log is at `/var/root/Library/Logs/Beacon/rippling-configure.log` on each Mac, readable only by root.

## Reference: configure script

Paste into **Beacon: configure** and fill in the five settings at the top.

```bash title="Beacon: configure" theme={null}
#!/bin/sh
# Beacon: configure S3 forwarding and the logged-in user's AI tools.
#
# Rippling script for macOS. Rippling runs it as root. Schedule it daily.
# Safe to run any number of times: it changes only what is missing or out of date.
#
# Rippling admins who can open this script can read the AWS key below. On the Mac the key
# goes only into a root-only file. It is never printed, never passed on a command line,
# and never placed in a process environment.

set -u

# ---- Settings: fill these in ---------------------------------------------------
BUCKET="<bucket>"
REGION="<region>"
PREFIX="<prefix>"                       # root prefix, without /runtime or /inventory
WRITER_KEY_ID="<access key id>"
WRITER_SECRET="<secret access key>"
# --------------------------------------------------------------------------------

BEACON="/opt/beacon/bin/beacon"
FORWARDER="/opt/beacon/jamf/claude/s3/install-forwarder.sh"
FWD_DIR="/Library/Application Support/Beacon/Forwarders"
CRED_FILE="$FWD_DIR/s3-credentials"
ENV_FILE="$FWD_DIR/s3-vector.env"
STATE_DIR="/Library/Application Support/Beacon/Rippling"
LABEL="com.beacon.endpoint.s3-forwarder"
RUNTIME_LOG="/var/log/beacon-agent/runtime.jsonl"
LOG_DIR="/var/root/Library/Logs/Beacon"
LOG="$LOG_DIR/rippling-configure.log"

say() { printf '%s\n' "$*"; }

if [ "$(id -u)" -ne 0 ]; then
  say "FAIL: must run as root"
  exit 1
fi
if pgrep -x installer >/dev/null 2>&1; then
  say "WAIT: a package install is running; a later run finishes setup"
  exit 1
fi
if ! pkgutil --pkg-info ai.asymptote.beacon.endpoint >/dev/null 2>&1 ||
  [ ! -x "$BEACON" ] || [ ! -x "$FORWARDER" ] || [ ! -x /opt/beacon/bin/vector ]; then
  say "WAIT: Beacon is not installed yet; a later run finishes setup"
  exit 1
fi
case "$BUCKET$REGION$PREFIX$WRITER_KEY_ID$WRITER_SECRET" in
  *"<"*) say "FAIL: fill in the settings at the top of the script"; exit 1 ;;
esac

# The same normalization Beacon's S3 helper applies, so the comparison below matches.
PREFIX="${PREFIX%/}"
case "$PREFIX" in
  */runtime) PREFIX="${PREFIX%/runtime}" ;;
  */inventory) PREFIX="${PREFIX%/inventory}" ;;
esac
PREFIX="${PREFIX%/}"
[ -n "$PREFIX" ] || PREFIX="beacon"

umask 077
mkdir -p "$LOG_DIR" "$FWD_DIR" "$STATE_DIR"
chmod 0700 "$LOG_DIR" "$STATE_DIR"
: >"$LOG"

rc=0
version="$("$BEACON" version 2>/dev/null | head -n 1)"

# ---- 1. Writer key -> root-only AWS credentials file ----------------------------
# printf is a shell builtin, so the key never appears in a process list.
printf '[default]\naws_access_key_id = %s\naws_secret_access_key = %s\n' \
  "$WRITER_KEY_ID" "$WRITER_SECRET" >"$CRED_FILE.new"
if cmp -s "$CRED_FILE.new" "$CRED_FILE"; then
  rm -f "$CRED_FILE.new"
else
  mv -f "$CRED_FILE.new" "$CRED_FILE"
fi
chown root:wheel "$CRED_FILE"
chmod 0600 "$CRED_FILE"

# ---- 2. S3 forwarder -------------------------------------------------------------
# A fingerprint of everything the forwarder was last set up with, recorded only after a
# successful setup. A new Beacon version, setting or key, or a failed setup, all differ.
want="$(printf '%s\n' "$version" "$BUCKET" "$REGION" "$PREFIX" "$WRITER_KEY_ID" "$WRITER_SECRET" |
  shasum -a 256 | awk '{print $1}')"

forwarder_current() {
  [ "$(cat "$STATE_DIR/forwarder" 2>/dev/null)" = "$want" ] || return 1
  [ -f "$ENV_FILE" ] || return 1
  launchctl print "system/$LABEL" 2>/dev/null | grep -q 'state = running' || return 1
  # Beacon writes this file with shell quoting; read it in a subshell.
  (
    unset AWS_SHARED_CREDENTIALS_FILE AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE
    . "$ENV_FILE"
    [ "${AWS_SHARED_CREDENTIALS_FILE:-}" = "$CRED_FILE" ] &&
      [ -z "${AWS_ACCESS_KEY_ID:-}${AWS_SECRET_ACCESS_KEY:-}${AWS_SESSION_TOKEN:-}${AWS_PROFILE:-}" ]
  )
}

if forwarder_current; then
  say "OK: S3 forwarder running with current settings"
else
  rm -f "$STATE_DIR/forwarder"
  # The rotated archives are included so the first upload has everything recorded since
  # Beacon was installed, even when the log rotated before this script first ran.
  if env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN -u AWS_PROFILE \
    BEACON_S3_BUCKET="$BUCKET" \
    AWS_REGION="$REGION" \
    BEACON_S3_PREFIX="$PREFIX" \
    BEACON_S3_STORAGE_CLASS="STANDARD" \
    BEACON_VECTOR_READ_FROM="beginning" \
    BEACON_RUNTIME_LOG_PATHS="$RUNTIME_LOG,$RUNTIME_LOG.[0-9]" \
    AWS_SHARED_CREDENTIALS_FILE="$CRED_FILE" \
    "$FORWARDER" >>"$LOG" 2>&1; then
    printf '%s\n' "$want" >"$STATE_DIR/forwarder"
    say "OK: S3 forwarder configured and running"
  else
    say "FAIL: S3 forwarder setup failed; details in $LOG on the Mac"
    err="/tmp/$LABEL.err"
    if [ -f "$err" ] && [ "$(stat -f %Su "$err")" = root ]; then
      grep ' ERROR ' "$err" | grep -vi healthcheck | tail -n 3 | cut -c1-300 | sed 's/^/  vector: /'
    fi
    rc=1
  fi
fi

# ---- 3. Logged-in user's Claude Code, Codex and Cursor ---------------------------
user="$(stat -f %Su /dev/console 2>/dev/null || true)"
case "$user" in
  "" | root | loginwindow | _mbsetupuser)
    say "WAIT: nobody is logged in; a later run configures the user's AI tools"
    ;;
  *)
    home="$(dscl . -read "/Users/$user" NFSHomeDirectory 2>/dev/null | awk '{print $2}')"
    tools="claude,codex"
    cursor=0
    # Cursor hooks are added only where Cursor is present, so inventory does not report it everywhere.
    if [ -d /Applications/Cursor.app ] || [ -d "$home/Applications/Cursor.app" ] || [ -d "$home/.cursor" ]; then
      tools="$tools,cursor"
      cursor=1
    fi
    # Reconfigure only when this user was never set up for this Beacon version and set of
    # tools, or a Beacon hook has gone missing, so the user's settings are not rewritten daily.
    stamp="$STATE_DIR/user-$user"
    current=1
    [ "$(cat "$stamp" 2>/dev/null)" = "$version $tools" ] || current=0
    grep -q 'beacon-hooks' "$home/.claude/settings.json" 2>/dev/null || current=0
    grep -q 'beacon-hooks' "$home/.codex/hooks.json" 2>/dev/null || current=0
    if [ "$cursor" -eq 1 ]; then
      grep -q 'beacon-hooks' "$home/.cursor/hooks.json" 2>/dev/null || current=0
    fi
    if [ "$current" -eq 1 ]; then
      say "OK: $tools already configured for $user"
    elif "$BEACON" endpoint user-config repair-installed --system --harness "$tools" >>"$LOG" 2>&1; then
      printf '%s\n' "$version $tools" >"$stamp"
      say "OK: $tools configured for $user"
    else
      rm -f "$stamp"
      say "FAIL: could not configure $tools for $user; details in $LOG on the Mac"
      rc=1
    fi
    ;;
esac

# ---- 4. Health summary (no event content) ----------------------------------------
if launchctl print system/com.beacon.endpoint.collector 2>/dev/null | grep -q 'state = running'; then
  say "OK: collector running"
else
  say "FAIL: collector not running"
  rc=1
fi
say "INFO: $version"
if [ -f "$RUNTIME_LOG" ]; then
  age=$(( $(date +%s) - $(stat -f %m "$RUNTIME_LOG") ))
  say "INFO: last local event ${age}s ago"
fi

exit "$rc"
```

## Reference: remove script

Paste into **Beacon: remove** as is.

```bash title="Beacon: remove" theme={null}
#!/bin/sh
# Beacon: remove from this Mac.
#
# Rippling script for macOS, run once as root. Take the Mac out of the Beacon software's
# and the configure script's Supergroups first, or Rippling reinstalls Beacon within the hour.
# Removes Beacon's hooks from every local user's Claude Code, Codex and Cursor, then Beacon's
# services, files, logs, package receipt and S3 key file. Ends with OK only when all are gone.

set -u

BEACON="/opt/beacon/bin/beacon"
CLEANUP="/opt/beacon/jamf/scripts/full-cleanup.sh"
LOG_DIR="/var/root/Library/Logs/Beacon"
LOG="$LOG_DIR/rippling-uninstall.log"

if [ "$(id -u)" -ne 0 ]; then
  echo "FAIL: must run as root"
  exit 1
fi

umask 077
mkdir -p "$LOG_DIR"
chmod 0700 "$LOG_DIR"
: >"$LOG"

local_users() {
  for home in /Users/*; do
    [ -d "$home" ] || continue
    u="$(basename "$home")"
    case "$u" in Shared | Guest | .localized) continue ;; esac
    id "$u" >/dev/null 2>&1 && printf '%s %s\n' "$u" "$home"
  done
}

if [ -x "$BEACON" ]; then
  # 1. Hooks first, with Beacon's own command, as each user so their files keep their owner.
  local_users | while read -r u home; do
    sudo -u "$u" HOME="$home" "$BEACON" endpoint hooks uninstall \
      --harness claude,codex,cursor --level user \
      --log-path /var/log/beacon-agent/runtime.jsonl >>"$LOG" 2>&1 ||
      echo "WARN: could not remove Beacon hooks for $u"
  done

  # 2. Everything else. The helper deletes /opt/beacon, so it runs from a root-only copy.
  # Without the Command Line Tools, /usr/bin/python3 would ask the user to install them, so
  # the helper is given a python3 that is absent instead; it then skips the optional edits
  # that remove Beacon's telemetry settings from Claude Code and Codex.
  work="$(mktemp -d /var/root/beacon-remove.XXXXXX)"
  cp "$CLEANUP" "$work/full-cleanup.sh"
  chmod 0700 "$work/full-cleanup.sh"
  helper_path="/usr/bin:/bin:/usr/sbin:/sbin"
  if ! xcode-select -p >/dev/null 2>&1; then
    mkdir "$work/bin"
    printf '#!/bin/sh\nexit 127\n' >"$work/bin/python3"
    chmod 0700 "$work/bin/python3"
    helper_path="$work/bin:$helper_path"
    echo "INFO: no Command Line Tools; Beacon's telemetry settings stay in Claude Code and Codex (harmless once Beacon is gone)"
  fi
  PATH="$helper_path" BEACON_CLEAN_ALL_USERS=1 /bin/sh "$work/full-cleanup.sh" >>"$LOG" 2>&1
  rm -rf "$work"
else
  echo "INFO: Beacon's program files are already gone; removing what is left"
fi

# 3. Whatever is left, whether or not the helper ran.
for label in collector inventory updater s3-forwarder gcs-forwarder falcon-forwarder asymptote-forwarder; do
  launchctl bootout "system/com.beacon.endpoint.$label" >/dev/null 2>&1 || true
  rm -f "/Library/LaunchDaemons/com.beacon.endpoint.$label.plist" \
    "/tmp/com.beacon.endpoint.$label.out" "/tmp/com.beacon.endpoint.$label.err"
done
rm -rf /opt/beacon "/Library/Application Support/Beacon" /var/log/beacon-agent
rm -f "$LOG_DIR/rippling-configure.log"
pkgutil --forget ai.asymptote.beacon.endpoint >/dev/null 2>&1 || true

# 4. Check.
rc=0
for label in collector inventory updater s3-forwarder gcs-forwarder falcon-forwarder asymptote-forwarder; do
  if launchctl print "system/com.beacon.endpoint.$label" >/dev/null 2>&1; then
    echo "FAIL: service com.beacon.endpoint.$label is still loaded"
    rc=1
  fi
done
for dir in /opt/beacon "/Library/Application Support/Beacon" /var/log/beacon-agent; do
  if [ -e "$dir" ]; then
    echo "FAIL: $dir still exists"
    rc=1
  fi
done
if pkgutil --pkg-info ai.asymptote.beacon.endpoint >/dev/null 2>&1; then
  echo "FAIL: package receipt remains"
  rc=1
fi
left="$(local_users | while read -r u home; do
  if grep -q 'beacon-hooks' "$home/.claude/settings.json" "$home/.codex/hooks.json" "$home/.cursor/hooks.json" 2>/dev/null; then
    printf ' %s' "$u"
  fi
done)"
if [ -n "$left" ]; then
  echo "FAIL: Beacon hooks remain for:$left. Put the Mac back in the Beacon Supergroup, let Beacon reinstall, then run this again."
  rc=1
fi

if [ "$rc" -eq 0 ]; then
  echo "OK: Beacon removed; details in $LOG on the Mac"
fi
exit "$rc"
```

## Reference: profile

Save as `beacon-background-items.mobileconfig` and upload it in step 3. It needs no edits and contains no secrets.

```xml title="beacon-background-items.mobileconfig" theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>PayloadContent</key>
  <array>
    <dict>
      <key>PayloadType</key>
      <string>com.apple.servicemanagement</string>
      <key>PayloadIdentifier</key>
      <string>ai.asymptote.beacon.managed-login-items.servicemanagement</string>
      <key>PayloadUUID</key>
      <string>19DCAEC1-5692-4BE6-96B6-674599CADB9D</string>
      <key>PayloadVersion</key>
      <integer>1</integer>
      <key>PayloadDisplayName</key>
      <string>Beacon background services</string>
      <key>Rules</key>
      <array>
        <dict>
          <key>Comment</key>
          <string>Beacon endpoint LaunchDaemons (collector, inventory, updater, S3 forwarder)</string>
          <key>RuleType</key>
          <string>LabelPrefix</string>
          <key>RuleValue</key>
          <string>com.beacon.endpoint.</string>
        </dict>
        <dict>
          <key>Comment</key>
          <string>Binaries signed by Asymptote Labs, Inc.</string>
          <key>RuleType</key>
          <string>TeamIdentifier</string>
          <key>RuleValue</key>
          <string>3A98D35XJR</string>
        </dict>
      </array>
    </dict>
  </array>
  <key>PayloadDisplayName</key>
  <string>Beacon Endpoint Agent - Managed Background Items</string>
  <key>PayloadDescription</key>
  <string>Approves Beacon's background services so they cannot be turned off in Login Items and users are not prompted.</string>
  <key>PayloadIdentifier</key>
  <string>ai.asymptote.beacon.managed-login-items</string>
  <key>PayloadOrganization</key>
  <string>Asymptote Labs, Inc.</string>
  <key>PayloadScope</key>
  <string>System</string>
  <key>PayloadType</key>
  <string>Configuration</string>
  <key>PayloadUUID</key>
  <string>0B1CA656-F35E-4AAA-AC35-499117A10B40</string>
  <key>PayloadVersion</key>
  <integer>1</integer>
</dict>
</plist>
```

## Related

<Columns cols={2}>
  <Card title="Rippling" icon="laptop" href="/mdm/rippling">
    What the package installs and how Rippling deploys it.
  </Card>

  <Card title="Try Beacon on One Mac" icon="laptop" href="/mdm/try-on-one-mac">
    Install the package by hand and see what it captures.
  </Card>

  <Card title="Beacon with Fleet and S3" icon="bucket" href="/guides/fleet-s3-mdm">
    The same S3 setup, installed with Fleet.
  </Card>

  <Card title="Configure S3 Data Connector" icon="bucket" href="/manage/configure-s3-data-connector">
    Give Asymptote read-only access to the bucket.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.