Cron jobs are one of Hermes’s most powerful features — scheduled agents that run autonomously on any cadence. But until recently, they had a security issue: jobs weren’t always running under the correct secret scope.
The Bug: How Secret Scoping Failed
When you schedule a Hermes cron job, it runs in its own session with its own profile. The problem was that run_one_job wasn’t scoping secrets to the job’s profile — it was falling back to a default scope. If you had different credentials configured per profile, your cron job might run with the wrong set of secrets.
Concrete Example
# Profile A: Personal projects - has access to personal GitHub token
hermes profile create personal --set-secret GITHUB_TOKEN=ghp_personal_xxx
# Profile B: Work projects - has access to work GitHub token
hermes profile create work --set-secret GITHUB_TOKEN=ghp_work_xxx
# Cron job assigned to 'work' profile
hermes cron add "daily-standup" --profile work --schedule "0 9 * * 1-5" \
--prompt "Generate daily standup summary for work repos"
Before the fix: The cron job might execute with personal profile’s secrets (the default fallback), posting work repo data to your personal GitHub, or worse — failing silently with wrong credentials.
After the fix: The cron job explicitly scopes to the work profile’s credential store before execution. No leakage, no confusion.
The Attack Surface
The vulnerability wasn’t just about wrong credentials — it was about credential escalation:
- Profile A (low privilege): Reads public repos only
- Profile B (high privilege): Admin access to private org repos
- Cron job assigned to Profile A but runs with Profile B’s secrets → privilege escalation
Worse, if a job used a profile with elevated permissions and the job’s output was delivered to a public channel (like a team Slack or a public GitHub issue), your credentials were effectively exposed through the agent’s output.
Real-World Impact Scenarios
Scenario: Multi-Client Agency An agency manages Hermes for three clients, each with their own profile and GitHub tokens. A cron job for Client A accidentally runs with Client B’s token — it could create issues, merge PRs, or read private repos in Client B’s organization. The agency wouldn’t know until a client complained.
Scenario: Personal + Work Separation You use Hermes for both personal open-source work and your day job. Your work profile has AWS credentials, Kubernetes configs, and internal API keys. Your personal profile has only a GitHub token. A cron job meant for personal blog updates runs with work credentials — suddenly your personal agent can provision AWS resources.
Scenario: CI/CD Pipeline Contamination
A cron job runs hermes run --prompt "deploy to staging" but executes with production profile secrets. The agent deploys to production instead of staging. This isn’t theoretical — it’s exactly the kind of silent misconfiguration that causes production incidents.
The Fix: Two Commits, One Security Boundary
Two commits addressed this:
-
fix(cron): run jobs under the profile secret scope— the core fix. Cron jobs now explicitly scope credentials to their assigned profile before executing. -
test(cron): regression test for run_one_job secret scope— a regression test that ensures this doesn’t break again.
What Changed Internally
The fix modifies the job execution pipeline in packages/core/src/cron/runner.ts:
// BEFORE (vulnerable)
async function runOneJob(job: CronJob) {
const session = await createSession(); // Uses default secret scope
return session.run(job.prompt);
}
// AFTER (secure)
async function runOneJob(job: CronJob) {
const profile = await loadProfile(job.profile);
const session = await createSession({
secretScope: profile.id, // Explicit profile scoping
credentials: profile.secrets
});
return session.run(job.prompt);
}
The secretScope parameter ensures the credential guard resolves secrets only from the assigned profile’s namespace.
The Test That Prevents Regression
The regression test in packages/core/src/cron/runner.test.ts:
test('run_one_job uses profile secret scope', async () => {
// Create two profiles with different secrets
const profileA = await createProfile('profile-a', {
secrets: { API_KEY: 'key-a' }
});
const profileB = await createProfile('profile-b', {
secrets: { API_KEY: 'key-b' }
});
// Create cron job assigned to profile-b
const job = await createCronJob({
profile: 'profile-b',
prompt: 'echo $API_KEY'
});
// Execute and capture output
const output = await runOneJob(job);
// Should output profile-b's key, NOT profile-a's
expect(output).toContain('key-b');
expect(output).not.toContain('key-a');
});
This test runs on every CI build. If someone accidentally reverts the scoping logic, the build fails.
Why This Matters for Agent Security
This is the kind of security bug that’s easy to miss in agent systems. The agent works, the jobs run, results deliver — everything looks fine. But credentials are leaking through a gap in profile scoping.
The Broader Pattern: Agent Identity Isolation
Hermes treats profile = security boundary. This fix reinforces that principle:
| Component | Security Boundary | Enforcement |
|---|---|---|
| Interactive sessions | Profile-scoped | ✅ Enforced |
| Cron jobs | Profile-scoped | ✅ Now enforced |
| Skill executions | Profile-scoped | ✅ Enforced |
| Dashboard API | Profile-scoped | ✅ Enforced |
| MCP servers | Profile-scoped | ✅ Enforced |
Before this fix, cron jobs were the only execution path without explicit profile scoping.
How This Compares to Other Agent Frameworks
| Framework | Cron/Scheduled Jobs | Secret Scoping |
|---|---|---|
| Hermes | ✅ Native, profile-scoped | ✅ Per-profile, enforced |
| Claude Code | ❌ No native scheduling | N/A |
| Cursor | ❌ No native scheduling | N/A |
| OpenHands | ✅ Via cron, but shared env | ⚠️ Global .env only |
| AutoGPT | ✅ Continuous mode | ⚠️ Single config |
Hermes is unique in treating scheduled execution as a first-class security boundary.
Deep Dive: How Profile Scoping Works Internally
Under the hood, Hermes uses a credential resolver chain that prioritizes profile-scoped secrets over global defaults:
// packages/core/src/secrets/resolver.ts
class CredentialResolver {
async resolve(scope: string, key: string): Promise<string | undefined> {
// 1. Profile-scoped secret (highest priority)
const profileSecret = await this.profileStore.get(scope, key);
if (profileSecret) return profileSecret;
// 2. Environment variable (fallback for local dev)
const envVar = process.env[key];
if (envVar) return envVar;
// 3. Global config (lowest priority, deprecated)
return this.globalConfig.get(key);
}
}
When a cron job executes, the secretScope parameter becomes the scope argument above. This means:
- Profile secrets win over environment variables
- No accidental fallback to another profile’s secrets
- Explicit opt-in required for cross-profile access (future feature)
The resolver also logs every resolution attempt for audit purposes:
{
"timestamp": "2026-08-20T09:00:00.123Z",
"scope": "work",
"key": "GITHUB_TOKEN",
"source": "profile-store",
"result": "resolved"
}
This audit trail lets you prove exactly which secret was used for every cron execution — critical for compliance and incident response.
How to Verify Your Setup
1. Check Your Cron Job Profiles
# List all cron jobs with their assigned profiles
hermes cron list --verbose
# Output example:
# NAME SCHEDULE PROFILE STATUS
# daily-standup 0 9 * * 1-5 work ✅ Active
# weekly-audit 0 2 * * 0 security ✅ Active
2. Audit Profile Secrets
# See what secrets each profile holds (values masked)
hermes profile secrets list --profile work
hermes profile secrets list --profile personal
# Verify no cross-contamination
# work profile should ONLY show work-related secrets
3. Test Secret Resolution
Create a test cron job that echoes its resolved secrets (safely):
hermes cron add "secret-audit-test" --profile work --schedule "*/5 * * * *" \
--prompt "List the names (not values) of all environment variables available to this session. Do not print values."
Check the job output — it should only show secrets from the work profile.
4. Validate with Audit Logs
# View cron job execution audit trail with secret scope info
hermes audit cron --since 7d --profile work --include-scope
# Sample output:
# 2026-08-20 09:00:00 | daily-standup | profile=work | scope=work | secrets=[GITHUB_TOKEN,SLACK_WEBHOOK]
# 2026-08-19 02:00:00 | weekly-audit | profile=security | scope=security | secrets=[AWS_KEY,GITHUB_TOKEN]
Related Hermes Security Features
This fix is part of a layered security architecture:
Credential Guard System
- Blocks
.envfiles from being read/uploaded (case-insensitive since v2026.07) - Scans working directories for secret patterns before agent execution
- Redacts secrets from logs and outputs automatically
Profile-Scoped Credential Stores
- Each profile has an isolated encrypted credential store
- Secrets never cross profile boundaries
hermes profile create --isolatedenforces strict isolation
Audit Logging
# View cron job execution audit trail
hermes audit cron --since 7d --profile work
Migration Checklist
If you’re running Hermes with cron jobs:
- Update to latest:
npm update -g @hermes-agent/cli(or your install method) - Audit cron assignments:
hermes cron list --verbose— verify each job has the correct--profile - Verify secret isolation: Run the secret-audit test above for each profile
- Review delivery channels: Ensure cron job outputs don’t go to public channels
- Rotate compromised secrets: If you suspect cross-profile leakage, rotate affected tokens
What’s Next
The Hermes team is working on:
- Per-job secret allowlists — restrict which secrets a specific cron job can access
- Secret rotation hooks — automatic credential rotation on schedule
- Cross-profile delegation — controlled, audited secret sharing between profiles (opt-in only)
Related Articles
- Your AI Agents Config Directory Is Now the Most Dangerous Place on Your Machine
- Coding Agent Security Checklist 2026 — The Operator’s Hardening Guide
- Hermes Just Plugged a Secret Leak You Probably Didn’t Notice
Related articles
- Beware: Hermes Agent Security Audit Uncovers Credential Bypass, Sandbox Escape, and Session Hijacking in 5 HIGH-Severity Findings
- Coding Agent Security Checklist 2026 — The Operators Hardening Guide
- Claude Code v2.1.224: Self-Hosted Runners, Cross-Session Messaging, and Tighter Secret Handling
Stop paying for AI subscriptions you barely use. aiFiesta is $12/mo for access to GPT, Claude, Gemini, Grok, DeepSeek, Perplexity, and more. One sub, every top model.