Sanitize macOS Metadata in Cloud Mac CI Artifacts

Sanitize macOS Metadata in Cloud Mac CI Artifacts

A build on a cloud Mac may pass and the archive may extract successfully, yet the recipient can still encounter ._Config.json, .DS_Store, or extended attributes of unknown origin during upload. The compiler is rarely the problem. The usual causes are Finder operations, copies across file systems, and differences in how archiving tools handle macOS metadata. The solution is not to run a broad cleanup against the workspace, but to split delivery into four auditable stages: staging, scanning, archiving, and verification.

Define What Can Enter the Deliverable

In addition to a name and content, a macOS file may carry extended attributes, resource forks, and Finder metadata. These may be harmless on the local file system but become AppleDouble sidecar files after the content is moved into a ZIP archive, shared drive, or object store.

Define artifact rules before packaging instead of deleting files as you go:

Object Default policy Reason
.DS_Store Reject Describes only the Finder directory view
._* Reject Often created when resource forks cross file systems
com.apple.quarantine Inspect and handle selectively May originate from a download tool or browser
com.apple.ResourceFork Decide by artifact type Usually unnecessary for standard configuration bundles, but some files may depend on it
Signed application bundles Preserve structure and verify again Indiscriminate cleanup increases the risk of signature failures

Cleanup must target an isolated staging directory, not the source workspace. This prevents an incorrect policy from modifying caches, dependency directories, or inputs required by the next build.

If the deliverable contains a signed application, verify its signature before staging. Regular documents, configuration files, and logs can use a stricter metadata rejection policy. Do not apply the same recursive deletion command to both categories.

Create a One-Time Staging Directory

The staging directory must belong exclusively to the current job and be removed when the job exits. Do not reuse a fixed path such as /tmp/release, because concurrent jobs may overwrite one another.

#!/bin/bash
set -euo pipefail

SOURCE="${1:?source path required}"
OUTPUT="${2:?output path required}"
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/bamini-artifact.XXXXXX")"
UNPACK="$(mktemp -d "${TMPDIR:-/tmp}/bamini-unpack.XXXXXX")"

cleanup() {
  rm -rf "$STAGE" "$UNPACK"
}
trap cleanup EXIT

/usr/bin/ditto --norsrc "$SOURCE" "$STAGE/payload"
find "$STAGE/payload" -type f -name '.DS_Store' -delete
find "$STAGE/payload" -type f -name '._*' -delete

mktemp gives every job a unique directory, while trap covers successful completion, failure, and interruption. Here, ditto --norsrc copies the content without deliberately carrying resource forks. A subsequent scan is still required because the input directory may already contain materialized ._ files.

If the workflow must deliver a complete application bundle, first verify that the copy policy preserves everything the application requires. A safer approach is to stage application bundles and regular attachments separately, then apply an appropriate policy to each.

Scan Extended Attributes Instead of Erasing Everything

xattr -cr is convenient, but it is also too broad. It removes both known quarantine attributes and other attributes that have not yet been incorporated into the inspection rules. The pipeline turns green, but the root cause remains hidden.

List attribute names first, then fail the job only for explicitly forbidden entries:

ATTR_REPORT="$STAGE/xattr.txt"

if /usr/bin/xattr -lr "$STAGE/payload" >"$ATTR_REPORT" 2>&1; then
  if grep -E 'com\.apple\.(quarantine|ResourceFork)' "$ATTR_REPORT"; then
    echo "disallowed macOS metadata found" >&2
    exit 41
  fi
fi

if find "$STAGE/payload" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
  grep -q .; then
  echo "hidden metadata files found" >&2
  exit 42
fi

This gate fails as soon as metadata is detected, which helps identify the step that introduced it. After confirming the source, apply a targeted fix during download, copying, or generation. If the workflow genuinely permits a specific extended attribute, add both the file scope and attribute name to an allowlist instead of exempting the entire directory.

Record the Source Stage

Run a scan after source checkout, after compilation, and after staging. The first stage where the anomaly appears defines the investigation boundary. Common sources include browsing directories through a graphical interface, copying from a non-native file system, and tools that download content before extracting it. Scanning only before final archiving can block contamination, but it cannot identify the responsible step.

Reopen and Inspect the Final Archive

A clean staging directory does not guarantee that the archiving tool will not reintroduce metadata. After creating the ZIP, inspect its member names, then extract it into a new directory and verify it again.

rm -f "$OUTPUT"
/usr/bin/ditto -c -k --norsrc --keepParent \
  "$STAGE/payload" "$OUTPUT"

/usr/bin/unzip -Z1 "$OUTPUT" >"$STAGE/members.txt"

if grep -E '(^|/)\.DS_Store$|(^|/)\._[^/]+$' "$STAGE/members.txt"; then
  echo "archive contains forbidden metadata files" >&2
  exit 43
fi

/usr/bin/ditto -x -k "$OUTPUT" "$UNPACK"

if find "$UNPACK" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
  grep -q .; then
  echo "extracted artifact failed metadata check" >&2
  exit 44
fi

Inspecting the member list is fast and works well as the first gate. Verification after extraction also covers path conversion and extraction behavior. Both checks are necessary because the upload system receives the archive itself, while users ultimately work with its extracted contents.

If the archive contains a signed application, locate the extracted .app and run strict signature verification again. Verifying only the copy that existed before packaging will not reveal changes caused by the archive options.

Integrate the Gate into a Repeatable Delivery Workflow

Keep the script interface fixed to two arguments—an input directory and an output file—and make every job produce the following evidence:

  1. The artifact path and type before staging.
  2. The extended attribute scan results.
  3. The ZIP member list.
  4. The hidden-file check results after extraction.
  5. The second signature verification result when an application bundle is present.
  6. The SHA-256 digest of the final archive.

Generate the digest with shasum -a 256 artifact.zip. It cannot prove that two ZIP files are semantically identical, but it can confirm that no byte-level changes occurred during upload, download, or handoff.

When running these jobs on a BAMini cloud Mac, first confirm the currently available configuration in the console. Then commit the script to the repository and invoke it from CI instead of relying on someone to remember a manual cleanup step. After a gate failure, retain the member list and attribute report, but delete temporary directories containing project data. Every new job must create a fresh staging directory so that residue from the previous run cannot affect the result.

The goal is not to eliminate every extended attribute. It is to ensure that the deliverable contains only content explicitly approved by the team. Staging isolation limits the scope of modifications, scans at multiple stages identify the source, and verification after extraction validates the actual delivery result. All three are required to prevent hidden metadata from repeatedly resurfacing through different copy paths.

Frequently asked questions

Should I run xattr -cr across the entire workspace?

No. It recursively removes every extended attribute, can destroy required resource forks, and may hide an upstream process defect. Clean only an isolated staging directory using an explicit allowlist.

Why do ._ files remain after deleting .DS_Store?

Files prefixed with ._ are AppleDouble sidecars, not Finder database files. Disable resource-fork copying during packaging and inspect the complete archive member list afterward.

When should a signed application be verified?

Verify the source application before staging and avoid blanket attribute removal inside its bundle. Extract the final archive into a fresh directory and perform strict signature verification again.

Dedicated physical nodes

Move your next build to a cloud Mac mini

Choose your configuration, billing cycle, and node region to run development, builds, and automation on a dedicated physical machine.

Rent now