
A rotation that breaks the running agent is worse than no rotation. Issue the new key, verify it works, swap it into the agent, and only then revoke the old one — with a timed rollback window in case the new credential misbehaves in production.
The Figment virtual Intel macOS environment at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk) is enough to run automation scripts, call vendor APIs and manage credentials. It runs real macOS, so the security CLI, the login Keychain and launchd all work. Compatible Intel toolchains (Xcode 26 or earlier) are usable on it; it is not just a relay to another Mac.
Upgrade to an M-series Mac from $95/month if the agent needs to execute ARM-only binaries, run Xcode 27, or use frameworks that require Apple silicon. The specific chip, RAM and disk at that entry price are confirmed in the quote rather than fixed in the catalog, and physical M-series requests are queued until supply is confirmed. For a small team with several agents and an on-call contact, the Managed Mac Team at $999/month includes two M-series Macs, two CI runners, team setup, backups, monitoring and one hour of monthly support.
For a quick entry-price sanity check against a dedicated-Mac vendor, CloudMac lists a dedicated M4 (16 GB / 256 GB) at $99.99/month or $999/year on cloudmac.org/pricing. The annual option is lower than 12 × $95; $29 and $95 are Figment's lower entry prices for different hardware classes, not the same M4 configuration. Pick the plan by the job: the $29 VM for credential work and scripting, M-series for ARM execution, Managed for the included CI setup and support time.
The VM is reached through the browser console. Run key-management scripts from the desktop terminal or from an outbound CI runner you connect to your own service. Do not type key values on a shell command line where they would land in history.
The security command manages the Keychain from the terminal with no GUI. Apple's Keychain Services documentation describes the storage model and access control; run man security on your deployed macOS version to confirm flags for that release.
Store an API key interactively:
# Add a generic-password item to the login keychain.
# -s service -a account -T pre-authorises a binary -w prompts for the value
security add-generic-password \
-s "openai-agent" \
-a "prod" \
-T /usr/bin/python3 \
-w
-T pre-authorises the named binary so background launchd jobs do not trigger a GUI prompt ("User interaction is not allowed"). -w with no argument makes the shell prompt for the value, so the secret never appears in history.
Read the key in a script:
key=$(security find-generic-password -s "openai-agent" -a "prod" -w)
Never dump the secret to a .env file, and never export the entire environment to disk — it will contain other credentials. If a key has to move to a CI service, transfer the secret name, then paste the value into that CI's secret manager by hand.
Work the stages in order. Verify each row before moving on.
| Stage | Action | Verify | Rollback if it fails |
|---|---|---|---|
| 1 — Generate | Create a new key in the provider dashboard | Key ID appears in the dashboard | Delete it in the dashboard |
| 2 — Store new | security add-generic-password -s "openai-agent" -a "staging" -w | find-generic-password … -a "staging" -w returns a value | security delete-generic-password -s "openai-agent" -a "staging" |
| 3 — Smoke test | Run one agent job against the staging account label | Job exits 0, API returns a valid response | Fix the key or config; do not continue |
| 4 — Promote | Point the agent config at staging; restart the agent | First production job exits 0 | Revert config to prod; restart |
| 5 — Monitor | Let the agent run through the rollback window (at least 2 hours; 24 hours for critical jobs) | No auth errors in the agent log | Revert config; keep prod key alive |
| 6 — Revoke old | Revoke the old key in the provider dashboard | Old key returns 401 on a direct request | None — rollback is no longer possible |
| 7 — Remove entry | security delete-generic-password -s "openai-agent" -a "prod" | find-generic-password for prod reports the item is missing | None |
| 8 — Rename | Delete staging; re-add the same value under account prod | Retrieval under prod succeeds | None |
Do not compress stages 3–5 on a Friday afternoon. If you cannot watch the rollback window, delay stage 6 to the next business day.
Also rotate on a schedule — every 90 days is a reasonable floor. Use a launchd calendar reminder so the deadline never drifts.
Your agent will log metadata. Make sure the secret is not one of the fields.
What belongs in a log line:
2026-11-10T14:32:01Z job=openai-agent account=prod status=200 tokens_in=1240 tokens_out=380 cost_usd=0.00052
What must never appear:
# BAD — key value is visible
Authorization: Bearer sk-proj-abc123...
OPENAI_API_KEY=sk-proj-abc123...
Retrieve the key into a local variable, use it only in the HTTP header, and keep it out of loggers. In Python, silence noisy HTTP debug with logging.getLogger("urllib3").setLevel(logging.WARNING) so request headers are not echoed.
Add a log-scan test to the pipeline: match every line against your key prefix (for example sk-proj-[A-Za-z0-9]{40,}). If the pattern ever hits, fail the job and fix the logging path before the next rotation.
Drop a lightweight reminder into ~/Library/LaunchAgents/. Apple's launchd Creating Jobs guide covers StartCalendarInterval; when it is an array of dicts, each dict fires independently.
<!-- com.example.key-rotation-reminder.plist -->
<plist version="1.0"><dict>
<key>Label</key><string>com.example.key-rotation-reminder</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>-e</string>
<string>display notification "90-day API key rotation due" with title "Agent maintenance"</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer><key>Day</key><integer>1</integer><key>Month</key><integer>1</integer></dict>
<dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer><key>Day</key><integer>1</integer><key>Month</key><integer>4</integer></dict>
<dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer><key>Day</key><integer>1</integer><key>Month</key><integer>7</integer></dict>
<dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer><key>Day</key><integer>1</integer><key>Month</key><integer>10</integer></dict>
</array>
</dict></plist>
Load it with launchctl load ~/Library/LaunchAgents/com.example.key-rotation-reminder.plist. The notification is a prompt to run the checklist above by hand — automatic rotation without stages 3–5 skips the human smoke test and is the main way rotations silently break production.
.env-Based Agentsecurity add-generic-password -w (prompted value).security find-generic-password … -w into a local variable — not os.environ, not a temp file..env and remove the variable from any shell profile, Dockerfile or systemd unit.Once the agent reads from the Keychain, the rotation workflow above replaces every "edit .env and restart" scramble.
Request a Mac hosting configuration or browse plan options.
Related reading