
Agent logs capture everything that goes wrong — and sometimes everything that goes right, including the API key that authorized the call. The fix is not a cleanup script after the fact; it is a structured log schema that never admits secret values, combined with a test corpus you run before the agent reaches production.
The Figment virtual Intel macOS environment at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk) is a direct, low-cost host for agents that write logs locally and ship them to a central store. It runs a real macOS userland on Linux/KVM, so compatible Intel toolchains (Xcode 26 or earlier) and standard CLI tools work the same way they do on a physical Intel Mac; it is not a jump host pointed at some other machine. Access is through the browser console today, which is adequate for editing a launchd plist, writing log-rotation config, and tailing a file.
Step up to an M-series Mac from $95/month when the agent needs ARM execution, GPU acceleration, or heavier concurrency. Specific chip, RAM and disk are confirmed in the configuration quote — do not assume a particular M-series part at the entry price. The Managed Build Mac at $249/month adds one M-series Mac, CI setup, backups, monitoring and 30 minutes of monthly support, which you can point at the agent logs as an additional signal.
Figment competes on the specific managed-CI work in that $249 plan, not on raw hourly rentals. If all you need is a tiny host that writes JSON log lines and runs a nightly grep, start on the $29 VM and request an upgrade when the workload needs it. All prices here are the monthly list prices on /mac-hosting; taxes and fees apply as stated in the actual offer.
A launchd user agent writes to whatever paths you set in StandardOutPath and StandardErrorPath. If you leave them unset, output is discarded. Always set both and keep them inside a directory the agent user owns. See Apple's Creating Launchd Jobs for the full plist key list, including StartCalendarInterval for scheduled runs.
Three log destinations to keep separate in your head:
| Destination | Path pattern | Redaction needed? |
|---|---|---|
| Agent stdout/stderr | /Users/agent/logs/agent.log (your choice) | Yes — this is the file you control |
| macOS Unified Log | In-memory ring buffer; persists to /var/db/diagnostics/ | Conditional — the private specifier hides values |
| HTTP debug output | urllib3, requests verbose mode, curl -v | Yes — Authorization headers appear here |
Apple's guide to generating log messages documents that dynamic string arguments to os_log and Logger are redacted as <private> by default on release builds; %{public}s opts an argument back in. That behaviour applies to the Unified Log, not to files you write yourself — nothing in os_log protects a line you append to agent.log with logging.FileHandler.
Have the agent emit one JSON object per line and decide ahead of time which fields are allowed at all.
| Field | Type | Safe to log? | Notes |
|---|---|---|---|
timestamp | ISO 8601 string | Yes | Always UTC |
job | string | Yes | Agent job name |
run_id | string | Yes | UUID per invocation |
status | int / string | Yes | HTTP status or ok/error |
model | string | Yes | e.g. gpt-4o-mini |
input_tokens | int | Yes | Count only |
output_tokens | int | Yes | Count only |
spent_usd | float (4 d.p.) | Yes | Cost figure, not a key |
tool_name | string | Yes | Which tool was called |
tool_args_summary | string | Conditional | Summarise; scrub paths with usernames or PII |
error_type | string | Yes | Exception class name |
error_message | string | Conditional | Pass through the redactor before emitting |
api_key | string | Never | Keep in Keychain or the CI secret manager |
authorization_header | string | Never | Do not log HTTP headers at debug in production |
request_body | string | Never | May contain the user's prompt or PII |
response_body | string | Never | May contain sensitive model output |
env_dump | string | Never | Never serialise the whole environment to a log |
A line that uses only the allowed fields:
{"timestamp":"2026-11-10T14:32:01Z","job":"openai-agent","run_id":"a3f8b2c1","status":200,"model":"gpt-4o-mini","input_tokens":1240,"output_tokens":380,"spent_usd":0.0005,"tool_name":"fetch_records","error_type":null}
The reliable strategy is never to admit a secret value into the log stream in the first place. For Python agents using the standard logging module:
import logging, re
# Quiet the libraries that like to dump headers at DEBUG.
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
# Patterns are applied in order. Test every change against the corpus below.
_SECRET_PATTERNS = [
(re.compile(r"sk-[A-Za-z0-9\-_]{20,}"), "sk-***"),
(re.compile(r"ghp_[A-Za-z0-9]{36,}"), "ghp_***"),
(re.compile(r"(?<=Bearer )[A-Za-z0-9\-_.~+/=]{10,}"), "***"),
]
class RedactingFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
msg = super().format(record)
for pattern, replacement in _SECRET_PATTERNS:
msg = pattern.sub(replacement, msg)
return msg
handler = logging.FileHandler("/Users/agent/logs/agent.log")
handler.setFormatter(RedactingFormatter("%(message)s"))
logging.getLogger().addHandler(handler)
Add a pattern for every credential format the agent handles. The three above cover OpenAI-style sk- keys, GitHub personal access tokens, and generic Bearer tokens.
A test corpus is a small file of sample strings — some that must be redacted, some that must pass through untouched — that you run the formatter against before the agent reaches production. Commit it with the code and run it in CI.
# test_corpus.txt
# Format: LINE_TYPE | raw input | expected output after redaction
#
# Lines that MUST be redacted:
SECRET | Authorization: Bearer sk-proj-AbCdEfGhIjKlMnOpQrStUv1234567890ab | Authorization: Bearer sk-***
SECRET | api_key="sk-ant-api03-xyzXYZ1234567890abcdefABCDEF" | api_key="sk-***"
SECRET | OPENAI_API_KEY=sk-proj-testkey12345678901234567890 | OPENAI_API_KEY=sk-***
SECRET | token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | token: ghp_***
SECRET | Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp | Bearer ***
#
# Lines that MUST NOT be changed:
SAFE | status=200 model=gpt-4o-mini spent_usd=0.0005 | status=200 model=gpt-4o-mini spent_usd=0.0005
SAFE | error_type=RateLimitError | error_type=RateLimitError
SAFE | input_tokens=1240 output_tokens=380 | input_tokens=1240 output_tokens=380
SAFE | job=openai-agent run_id=a3f8b2c1 | job=openai-agent run_id=a3f8b2c1
Run the corpus through the formatter in a test. This must exit non-zero on any mismatch:
# test_redaction.py
import sys
from agent_logging import _SECRET_PATTERNS # your patterns module
def apply_redaction(text: str) -> str:
for pattern, replacement in _SECRET_PATTERNS:
text = pattern.sub(replacement, text)
return text
failures, checked = [], 0
with open("test_corpus.txt") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "|" not in line:
continue
line_type, raw, expected = [p.strip() for p in line.split("|", 2)]
checked += 1
result = apply_redaction(raw)
if result != expected:
failures.append(f"{line_type}: input={raw!r} expected={expected!r} got={result!r}")
if failures:
print("\n".join(failures))
sys.exit(1)
print(f"All {checked} corpus lines passed.")
Every new provider integration earns one or two new corpus lines. The test fails loudly if a new key shape leaks through.
For log files written before the formatter was in place, rewrite them once before archiving or sharing:
#!/bin/bash
# redact_existing_log.sh
set -euo pipefail
INPUT="$1"
OUTPUT="${INPUT%.log}-redacted.log"
sed -E \
-e 's/sk-[A-Za-z0-9_-]{20,}/sk-***/g' \
-e 's/ghp_[A-Za-z0-9]{36,}/ghp_***/g' \
-e 's/(Bearer )[A-Za-z0-9_.~+/-]{10,}/\1***/g' \
"$INPUT" > "$OUTPUT"
diff -u "$INPUT" "$OUTPUT" | head -40
echo "Redacted log written to: $OUTPUT"
The diff preview shows exactly what changed; keep the original only until you have reviewed it.
Schedule a nightly scan that greps current log files for the known secret shapes and writes a line to a scan log. Use a StartCalendarInterval key at 03:00:
#!/bin/bash
# nightly_secret_scan.sh
set -euo pipefail
LOG_DIR=/Users/agent/logs
PATTERNS='sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{36,}|Bearer [A-Za-z0-9_.~+/-]{15,}'
STAMP="$(date -u +%FT%TZ)"
if grep -rqE "$PATTERNS" "$LOG_DIR" 2>/dev/null; then
echo "$STAMP SECRET LEAK DETECTED in $LOG_DIR" >> "$LOG_DIR/security-scan.log"
exit 1
fi
echo "$STAMP scan clean" >> "$LOG_DIR/security-scan.log"
A hit here is a credential-rotation event, not a redaction event: rotate the leaked key in the issuing system first, then rewrite the affected log files with the post-processor above.
The $29 virtual Intel macOS plan runs this whole setup — launchd job, Python formatter, corpus test, nightly scan — on a Mac environment you control. If you want the nightly scan results piped into managed monitoring with human follow-up, the Managed Build Mac at $249/month includes monitoring and 30 minutes of monthly support. Browse Mac hosting plans or request your configuration.
Related reading