Engineering Article

Diagnosing and Safely Releasing CI Tools Blocked by Gatekeeper on a Cloud Mac

Diagnosing and Safely Releasing CI Tools Blocked by Gatekeeper on a Cloud Mac

A build task may run normally in an interactive terminal but fail on an unattended runner with errors such as “cannot be opened” or “operation not permitted,” or the process may exit immediately after launch. The first reaction is often to run chmod +x. However, when the file already has execute permission, the actual blocker is usually Gatekeeper, code-signing assessment, or a quarantine attribute attached to a downloaded file. Common triggers include downloading tools through a browser, copying artifacts from another session, and restoring caches from archives that preserve extended attributes.

Distinguish Between Four Types of Execution Failure First

Do not remove attributes as soon as a file “cannot execute.” First collect its file type, permissions, architecture, and extended attributes. These four checks are enough to rule out most misdiagnoses.

TOOL="/opt/build-tools/example-tool"

ls -leO@ "$TOOL"
file "$TOOL"
uname -m
xattr -l "$TOOL"

Use ls to confirm ownership and execute bits. The output of file should show an executable architecture that matches the node. If xattr lists com.apple.quarantine, that only means the file has entered the quarantine assessment process; it does not prove that the file is safe. For a Permission denied error, first check whether the parent directories allow traversal and whether the mount point has execution restrictions. For Bad CPU type in executable, obtain an artifact built for the correct architecture instead of modifying quarantine attributes.

Symptom Check first Do not do immediately
Permission denied File and parent-directory permissions Recursively remove extended attributes
Bad CPU type file and uname -m Repeatedly change execute bits
Damaged or cannot be verified Signature and Gatekeeper results Disable system security checks
Fails only on the runner Actual path, user, and cache source Assume the interactive environment is identical

A quarantine attribute is evidence about a file’s origin, not the failure itself. First prove that you have the expected file, then decide whether to allow it to run.

Obtain Gatekeeper’s Explicit Assessment

Assess a Single Binary or Application

For a command-line tool, inspect its signature first and then ask the system policy engine for an assessment. For an application bundle, set TOOL to the corresponding .app path.

codesign --display --verbose=4 "$TOOL"
codesign --verify --deep --strict --verbose=2 "$TOOL"
spctl --assess --type execute --verbose=4 "$TOOL"

An unsigned internal tool is not necessarily unsafe, but it must come from a controlled build process, with integrity established through checksum verification. If a third-party prebuilt tool is supposed to be signed but verification fails, stop using it and obtain a trusted artifact again. Do not hide a signature change by removing its quarantine attribute.

Query the Policy Logs

Dialogs and runner standard error often omit the underlying reason. Immediately after reproducing the failure, inspect the latest security policy records. This reveals the actual path that was assessed and prevents you from inspecting cache A when the runner actually executed a file from cache B.

log show --last 10m \
  --predicate 'subsystem == "com.apple.security.syspolicy"' \
  --style compact

Compare the path in the logs against command -v, the runner configuration, and script variables. Symbolic links are a particularly common source of discrepancies: the entry point may be in the tools directory while the final file comes from an old cache.

Verify the Artifact Before Allowing It to Run

The safest workflow is to obtain a SHA-256 checksum from the tool publisher or an internal artifact pipeline and store the checksum file in repository configuration alongside the tool version. Do not calculate a checksum from a downloaded file and then use it to verify that same file. That only proves the file did not change between reads; it does not prove that it came from the correct source.

cd /opt/build-tools
shasum -a 256 -c example-tool.sha256
codesign --verify --deep --strict --verbose=2 example-tool

Internally compiled tools may not have an external distribution signature, but you should at least retain the commit revision, build-script version, and expected checksum. If the tool is distributed as an archive, verify the archive first, extract it into a temporary directory, inspect the final executable, and then atomically replace the production path. This prevents a runner from reading a partially written file during an update.

Remove Quarantine Only From the Verified Target

After confirming that the checksum, version, and signature match expectations, modify only the verified target. Read the existing value and write it to the job log before deleting the specific attribute.

TOOL="/opt/build-tools/example-tool"

xattr -p com.apple.quarantine "$TOOL" 2>/dev/null || true
if xattr -p com.apple.quarantine "$TOOL" >/dev/null 2>&1; then
  xattr -d com.apple.quarantine "$TOOL"
fi

spctl --assess --type execute --verbose=4 "$TOOL"
"$TOOL" --version

Do not run xattr -cr . on the workspace. That command removes multiple extended attributes from the repository, dependencies, scripts, and temporary artifacts at the same time. It both broadens the release scope and destroys origin evidence needed for later investigation. An application bundle may need inherited attributes removed from files inside the bundle, but the scope should still be limited to a single verified bundle rather than the runner’s entire cache root.

Turn the Checks Into an Installation Gate

A reliable cloud Mac CI environment should not apply ad hoc permission fixes inside every build job. Move tool preparation into a dedicated installation stage: download into a temporary directory, verify the checksum, check the architecture, validate the signature, inspect quarantine status, release only the specific target, run a version self-check, and only then write the tool into the shared tools directory.

At minimum, the cache key should include the tool name, version, CPU architecture, and checksum. Even after a cache hit, run lightweight checks because someone may have manually overwritten the cached file. At runner startup, you can record id -un, uname -m, the resolved tool path, and its version, but do not print credentials or complete environment variables.

Finally, retain three failure rules: stop immediately when the checksum does not match; reacquire the artifact when a signature is expected but verification fails; and remove the target attribute only when quarantine assessment blocks an artifact that has already been verified. This approach requires a few more commands than globally disabling checks, but it makes the tool’s origin, the release action, and the version actually executed fully auditable.

Frequently asked questions

Why can macOS block a tool that already has execute permission?

Execute permission only controls a filesystem capability. Gatekeeper also evaluates quarantine metadata, code signatures, provenance, and system policy, so chmod +x cannot resolve every denial.

Is it safe to run xattr -cr on the entire CI workspace?

No. It removes extended attributes from every file, including unverified artifacts. Validate the checksum and signature first, then remove only com.apple.quarantine from the specific target.

How can repeated Gatekeeper blocks for the same tool be prevented?

Place download, checksum validation, extraction, and targeted release in a controlled installation stage, then cache the accepted file with a key containing its version, architecture, and checksum.

OrbVPS Cloud Mac

Run builds on dedicated Apple Silicon physical nodes

Configure nodes by model, region, and rental period. Actual availability is confirmed in real time by the control panel.

Rent a Cloud Mac now