After every job on a self-hosted Mac runner, delete the temporary keychain and the provisioning profile the job installed, and run that step even when the build fails. Hosted runners are destroyed after each job; yours is not, so leftover signing material stays on disk for the next job to find.
Why self-hosted is different
A hosted CI service's signing guide states the difference. Its hosted runners are isolated virtual machines destroyed at the end of the job, so certificates and profiles are destroyed with them. On self-hosted runners the temp directory is cleaned up, but the keychain and provisioning profile might still exist. The guide tells you to add a final step that deletes them.
Its secure-use guide goes further: self-hosted runners do not have guarantees around ephemeral clean virtual machines and can be persistently compromised by untrusted code, so they should almost never be used for public repositories. Be cautious for private repositories too, since anyone who can trigger a workflow may be able to run code there.
The per-job cleanup
The example cleanup in the guide uses two actions: delete the keychain created for the job, and remove the copied provisioning profile. Run it with an always condition so a failed build still cleans up. In outline:
- Delete the job's keychain.
- Remove the profile from the user's provisioning profiles folder.
- Delete any decoded certificate or key files in the temp directory.
- Confirm the keychain no longer appears in the search list.
Weekly checks on the machine
| Check | What a bad result looks like |
|---|---|
| List keychains in the user's search list | Old job keychains still present |
| List files in the provisioning profiles folder | Profiles from jobs long finished |
Look for .p12, .p8, .keystore files on disk | Any copy outside the secret manager |
| Review who can trigger jobs on this runner | People or forks that should not |
| Check the OS and Xcode patch level | Behind your update policy |
| Review environment variables in the runner service | Long-lived credentials set machine-wide |
Reduce the exposure
- Prefer ephemeral runners that are rebuilt from an image per job, where your tooling allows it.
- Dedicate a runner to release builds and keep pull request builds elsewhere. Our post on fork pull requests and signing secrets explains the split.
- Create a separate macOS user for the runner service, with no access to personal keychains.
- Keep the machine off networks it does not need to reach.
- Record who has physical or remote access.
If you think a runner was used badly
Treat it like a signing compromise: remove it from the pool, revoke the certificates it could reach, rotate store credentials, and rebuild the machine from a clean image before it returns. The steps in our post on leaked iOS certificates apply.
Where Safeguard fits
Safeguard does not manage runners. It scans the dependencies your pipeline builds with and applies policy gates. Try a free scan.
Quick check
Is deleting the temp folder enough?
No. The guide says the temp directory is cleaned, but the keychain and profile might remain, so delete them explicitly.
Can a self-hosted runner serve a public repository?
The documentation says it should almost never be used for public repositories, because anyone can open a pull request against it.
Sources
Last reviewed: 19 October 2026.