After the same Xcode build job has run continuously for several days, it may suddenly start reporting Permission denied when writing to DerivedData, updating dependency caches, or deleting old artifacts. Checking out the repository again may provide temporary relief, only for the issue to return during the next parallel job. Do not immediately blame Xcode or broaden directory permissions. More often, the root cause is a script that switched execution users, inherited a different umask, or left an additional ACL on the workspace.
Capture the Failure State First
Permission issues are easily erased by “cleaning up and rerunning.” First record the job’s execution identity, home directory, current directory, and default permission mask. Then inspect the first path that failed rather than scanning the entire disk immediately.
printf 'user=%s
' "$(id -un)"
printf 'groups=%s
' "$(id -Gn)"
printf 'home=%s
' "$HOME"
printf 'cwd=%s
' "$PWD"
printf 'umask=%s
' "$(umask)"
target="${FAILED_PATH:?set FAILED_PATH first}"
stat -f 'owner=%Su group=%Sg mode=%Sp path=%N' "$target"
ls -led "$target"
stat answers “who owns it and what is its basic mode,” while ls -le displays its ACL. A directory mode that appears writable does not guarantee successful access: a parent directory may lack execute permission, an ACL may contain a restrictive entry, or the file may belong to another job user.
Inspect only the first failing path and each of its parent directories. Recursively changing permissions across the entire workspace destroys evidence and may expose credentials, caches, and build artifacts to unrelated processes.
Distinguish Between Three Types of Permission Drift
Ownership Has Changed
The most common case is a step that installs dependencies or copies files through a privileged command, then leaves the resulting files owned by another user. Start by listing content that does not belong to the current job user:
workspace="${WORKSPACE:?set WORKSPACE first}"
find "$workspace" -x ! -user "$(id -un)" -print
If the output is concentrated in a single job directory, trace it back to the script that created it instead of hiding the source with a recursive chown. Ideally, a CI workspace should belong to the job user from the moment it is created. Even when privileged operations are unavoidable, their output should not be written back into the repository, caches, or DerivedData.
ACLs Exceed Expectations
Finder operations, migration scripts, and copy utilities may preserve ACLs. If numbered entries appear below the basic permission line in ls -le, determine where they came from. Remove ACLs only from a specific directory that is known to be a rebuildable temporary workspace:
job_dir="${JOB_DIR:?set JOB_DIR first}"
chmod -RN "$job_dir"
Do not run this command on a user’s home directory or a credentials directory. After the repair, run ls -led again to verify that the ACL is gone and the basic mode still meets requirements.
The umask Is Inconsistent
Interactive SSH sessions, CI daemons, and standalone scripts do not necessarily load the same shell configuration. If one job creates a cache with 077, a later job may be unable to reuse it even when both users belong to the same group. Instead of relying on startup files, declare the mask explicitly at the job entry point:
umask 022
install -d -m 0755 "$JOB_DIR"
install -d -m 0755 "$JOB_DIR/DerivedData"
install -d -m 0755 "$JOB_DIR/Artifacts"
Sensitive material should use a separate directory with a stricter mode. Do not weaken permissions across the board merely to share build caches.
Use a Separate Workspace for Every Job
When parallel jobs share a DerivedData or archive directory, one job’s cleanup may collide with files another job is still writing. Build the directory name from the repository identifier, commit identifier, and job number, then pass the resulting paths explicitly to the build command.
run_id="${CI_RUN_ID:?set CI_RUN_ID first}"
root="$HOME/ci-runs/$run_id"
src="$root/source"
derived="$root/DerivedData"
artifacts="$root/Artifacts"
umask 022
install -d -m 0755 "$src" "$derived" "$artifacts"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-derivedDataPath "$derived" \
archive \
-archivePath "$artifacts/App.xcarchive"
Keep shared caches separate from job output. Dependency download caches may be reused read-only, while DerivedData, archives, and logs should be isolated per job. This reduces cross-job permission contamination and preserves the failure state until the job ends.
| Path type | Recommended owner | Lifecycle | Concurrent writes allowed |
|---|---|---|---|
| Source checkout | Individual job | Single run | No |
| DerivedData | Individual job | Single run | No |
| Archives and logs | Individual job | Clean up after validation | No |
| Download cache | Dedicated job user | Across jobs | Updated only by the cache workflow |
Set Validation Gates Before and After the Build
Permission checks should be part of the job entry point rather than a manual response after a failure. Before the build, confirm that the directory is writable, has the correct owner, and contains no unexpected ACLs. After the build, check whether any files were created under a different owner. The checks below terminate the job immediately when drift is detected:
test -d "$JOB_DIR"
test -w "$JOB_DIR"
unexpected_owner="$(
find "$JOB_DIR" -x ! -user "$(id -un)" -print -quit
)"
if [ -n "$unexpected_owner" ]; then
printf 'unexpected owner: %s
' "$unexpected_owner" >&2
exit 1
fi
if ls -led "$JOB_DIR" | tail -n +2 | grep -q '^[[:space:]]*[0-9]:'; then
printf 'unexpected ACL on job directory
' >&2
exit 1
fi
If the team needs a shared cache, define its writer, update schedule, and atomic replacement method separately. Do not allow every build job to modify the same directory simultaneously. Cleanup after a job should delete only the path associated with the current run_id, after verifying that the path is beneath the expected root directory.
Fix the Script That Introduced the Problem
A reliable fix should answer four questions: which step created the anomalous file, which user created it, what umask it inherited, and why it was written to a shared directory. Once those answers are known, encode directory creation, execution identity, and output locations in the script instead of depending on the environment left by an interactive login.
When running CI on RunnerVM cloud Macs, similarly print a minimal environment snapshot at the start of each job and confirm the currently available configurations in the console. After a machine restart or job migration, the build should not depend on state left by the previous run as long as the entry script can recreate the directory and permission baseline. The ultimate goal is not to eliminate all permission restrictions, but to make every file’s creator, read/write scope, and cleanup responsibility predictable.
Frequently asked questions
Should I run chmod 777 on the entire workspace after a Permission denied error?
No. It hides ownership and ACL defects while allowing unrelated processes to modify build files. Inspect the failing path with stat and ls -le, then repair only the confirmed job directory.
Why does a script work over SSH but fail inside a CI job?
The CI process may have a different user, HOME, umask, PATH, and launch environment. Record those values at job startup and set the required permission baseline explicitly in the script.
Run your next build on a dedicated physical Mac
Choose a cloud Mac node for each task duration, then manage orders, connection details, and support tickets from the console.