Replicable infrastructure for running a Hermes Agent as a reactive GitHub bot on the security-alliance/frameworks repository.
Anyone can clone this, run setup.sh, and have their own reactive agent with their own bot identity, whitelist, and signing key -- all backed by the same relay + tunnel architecture.
- A Hermes profile named frameworks with a reactive GitHub agent personality (customized to your bot identity during setup)
- A GitHub webhook relay that receives events and dispatches them to Hermes
- A Cloudflare tunnel to expose the relay to GitHub webhooks
- GPG-signed commits from your bot identity
- systemd user services for the relay and tunnel
- Three layers of review/comment dedup protection
- Dangerous command auto-deny with audit logging
- Stuck spawn detection with automatic rescue
- Model fallback chain for resilience
- Hermes Agent installed (
pip install hermes-agentor from source) - GitHub account for the bot with a classic PAT (repo, workflow, read:org scopes)
- GPG key for signing commits (create one or import an existing one during setup)
- Cloudflare tunnel token (if using cloudflared for tunneling)
- Python 3.9+ with
aiohttpandpyyaml - gh CLI authenticated (
gh auth login) - git with GPG signing support
git clone https://github.com/frameworks-volunteer/frameworks-hermes.git
cd frameworks-hermes
./setup.shThe setup script walks you through 7 steps:
- Hermes profile -- imports the
frameworksprofile skeleton and generates your SOUL.md from a template - GPG key -- selects or creates a signing key
- Git config -- sets up your bot identity and signing
- Relay -- symlinks relay code to
~/ops/frameworks-gh-relay/, generates config.env from template - Frameworks repo -- clones with fork/origin and upstream remotes
- systemd services -- installs relay + tunnel units
- Start -- optionally starts services
Everything that is user-specific (bot name, GPG key, PAT, webhook secret, tunnel token, sender whitelist) is prompted during setup. Nothing is hardcoded.
GitHub webhook --> Cloudflare tunnel --> relay (port 9191)
| |
| 1. Verify signature
| 2. Dedupe (SQLite)
| 3. Check repository
| 4. Self-event filter
| 5. Whitelist check
| 6. Classify event
| 7. Choose model
| 8. Build prompt
| 9. Enqueue work
|
v
Worker thread picks up work item
|
v
Spawn Hermes (PTY) with profile + skills
|
v
Monitor output:
- Auto-deny dangerous commands
- Track completed actions (dedup guard)
- Detect stuck spawns
- Kill-on-rescue (prevent race conditions)
|
v
Agent uses gh CLI / git --> push to fork --> PR to upstream
frameworks-hermes/
setup.sh # Main setup script (7 steps)
README.md # This file
docs/
opencode-worker-design.md # Future: OpenCode as relay worker (design doc)
profile/
frameworks.tar.gz # Hermes profile skeleton (SOUL.md only, no identity)
SOUL.md.template # Agent personality template ({{BOT_USERNAME}}, {{ALLOWED_SENDERS}})
relay/
relay.py # Webhook relay server (~1175 lines)
test_relay.sh # Integration test (filter validation)
config.env.template # Config template (all secrets removed)
.gitignore # Excludes config.env, deliveries.db, spawns/, logs
systemd/
frameworks-gh-relay.service # Relay service (paths: REPLACE_ME)
cloudflared-frameworks.service # Tunnel service (token: CLOUDFLARE_TUNNEL_TOKEN)
Every incoming webhook goes through this pipeline before any Hermes spawn:
| Step | Check | Result if failed |
|---|---|---|
| 1 | HMAC signature verification | 403 |
| 2 | Delivery ID dedup (SQLite, 48h) | 200 "duplicate" |
| 3 | Repository must match ALLOWED_REPO | 200 "wrong repo" |
| 4 | Sender must not be BOT_USERNAME | 200 "self-event" |
| 5 | Sender must be in ALLOWED_SENDERS | 200 "not whitelisted" |
| 6 | Event type + action classification | 200 "not in scope" |
| 7 | Model selection + prompt building | -- |
If all checks pass, the relay responds with 202 "accepted" and enqueues the work item.
| GitHub event | Action | Condition | Agent scope |
|---|---|---|---|
| issues | assigned | assignee == BOT_USERNAME | issue_assigned |
| pull_request | assigned | assignee == BOT_USERNAME | pr_assigned |
| pull_request | review_requested | requested_reviewer == BOT_USERNAME | pr_review_requested |
| issue_comment | created | body mentions @BOT_USERNAME or contains trigger phrases | issue_comment |
| pull_request_review | submitted | body mentions @BOT_USERNAME or contains trigger phrases | pr_review |
| pull_request_review_comment | created | body mentions @BOT_USERNAME or contains trigger phrases | pr_review_comment |
Trigger phrases for comments/reviews: "please fix", "please review", "please look", "take a look", "can you", "could you", "needs review", or the bot's username.
The relay tries models in order from MODEL_CHAIN (config.env). If the primary model fails or gets rate-limited (429), it falls back to the next one automatically. Example chain:
openrouter/z-ai/glm-5.1 -> openrouter/minimax/MiniMax-M2.7 -> openrouter/kimi-coding-cn/kimi-k2.5
When the bot reviews its own PRs (self-review), it uses SELF_REVIEW_MODELS instead -- alternate models to avoid using the same model that wrote the code.
Dangerous command auto-deny:
The relay monitors the PTY output for Hermes's "DANGEROUS COMMAND:" prompts (the [o/s/D] choice that Hermes shows before executing shell commands). When one appears, the relay automatically sends d (deny) to the prompt within milliseconds. Denied commands are logged to dangerous_cmds.log for audit. This prevents the agent from running destructive commands like rm -rf, force pushes, etc.
Stuck spawn detection:
If a Hermes spawn produces no output for STUCK_TIMEOUT seconds (default 180), the relay:
- Kills the stuck process immediately (prevents race conditions)
- Spawns a rescue agent with a different model from the fallback chain
- The rescue agent reads the stuck spawn's output log, diagnoses the issue, and either completes the task or leaves a comment explaining what happened
Kill-on-rescue:
The original spawn is killed BEFORE the rescue agent runs. This prevents both from operating simultaneously and submitting duplicate reviews/comments.
Review/comment dedup (3 layers):
- Prompt-level: The agent's prompt instructs it to check for existing reviews before submitting, and to never submit a duplicate.
- Rescue prompt: The rescue agent checks the original spawn's output for successful review/comment submissions. If found, it is told "DO NOT submit another review."
- PTY-level hard guard: The relay tracks every
gh pr review,gh pr comment, andgh issue commentcall in acompleted_actionsset. If the agent attempts a second call of the same type in the same spawn, the relay kills the process immediately.
GH body rule (--body-file heredoc):
The agent is instructed to never use --body with inline text, because double quotes cause bash to expand backticks as command substitution (which caused a triple-review incident). Instead, the agent always uses:
cat > /tmp/${SPAWN_ID}_body.md << 'EOF'
(body content with backticks, dollar signs, etc.)
EOF
gh pr review NUM --approve --body-file /tmp/${SPAWN_ID}_body.mdThe single-quoted 'EOF' prevents ALL shell expansion. The $SPAWN_ID env var (unique per spawn) prevents filename collisions when multiple spawns run concurrently.
The relay enforces MAX_CONCURRENT (default 3) parallel Hermes processes. If the queue is full, new work items wait. Queue depth is logged and a warning is emitted at depth >= 10.
All runtime configuration lives in ~/ops/frameworks-gh-relay/config.env:
| Variable | Description | Default |
|---|---|---|
| GITHUB_WEBHOOK_SECRET | Secret from the GitHub webhook settings | (required) |
| GITHUB_TOKEN | Classic PAT with repo, workflow, read:org | (required) |
| ALLOWED_REPO | Repository full name | security-alliance/frameworks |
| BOT_USERNAME | Your bot's GitHub username | (required) |
| ALLOWED_SENDERS | Comma-separated whitelisted usernames | (required) |
| MODEL_CHAIN | Fallback chain: provider/model pairs | (see template) |
| SELF_REVIEW_MODELS | Models for reviewing bot's own PRs | (see template) |
| HERMES_BIN | Path to hermes binary | ~/.../hermes |
| REPO_PATH | Local path to the frameworks clone | ~/frameworks |
| RELAY_PORT | Port the relay listens on | 9191 |
| DELIVERY_DB | Path to dedup SQLite database | deliveries.db |
| LOG_FILE | Relay log path | relay.log |
| DANGEROUS_CMD_LOG | Denied commands audit log | dangerous_cmds.log |
| STUCK_TIMEOUT | Seconds with no output before rescue | 180 |
| MAX_CONCURRENT | Max parallel Hermes processes | 3 |
| MAX_SPAWN_SECONDS | Hard kill timeout per spawn | 900 |
See relay/config.env.template for the full list with descriptions.
The agent must sign all commits with a GPG key. You have two options:
Option A: Create a new key during setup
setup.sh will offer to list your existing keys. If you don't have one, create it:
gpg --full-generate-key
# Choose: RSA and RSA, 4096 bits, no expiration
# Use your bot's email: USERNAME@users.noreply.github.comOption B: Import an existing key
If you're migrating from another machine, export the key first:
# On the source machine:
gpg --armor --export-secret-keys KEY_FINGERPRINT > my-bot-secret-key.asc
gpg --armor --export KEY_FINGERPRINT > my-bot-public-key.asc
# Transfer both files securely to the new machine, then:
gpg --import my-bot-public-key.asc
gpg --import my-bot-secret-key.ascAfter import, verify the key is available:
gpg --list-secret-keys
gpg --sign --detach-sign --armor -u KEY_FINGERPRINT /dev/nullThe key must have no passphrase (the agent runs non-interactively and cannot enter a passphrase).
The relay needs a webhook registered on the upstream repository pointing to your tunnel URL. You need admin or repo admin permission on the upstream repository to register webhooks.
After setup.sh completes, it prints a curl command. Or register manually:
curl -X POST \
-H "Authorization: token YOUR_GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/security-alliance/frameworks/hooks \
-d '{
"name": "web",
"active": true,
"events": ["issues","pull_request","issue_comment","pull_request_review","pull_request_review_comment"],
"config": {
"url": "https://YOUR_TUNNEL_URL/webhook",
"content_type": "json",
"secret": "YOUR_WEBHOOK_SECRET"
}
}'Replace:
YOUR_GITHUB_TOKEN-- classic PAT with repo admin scopeYOUR_TUNNEL_URL-- your cloudflared tunnel URL (e.g.fwks.example.com)YOUR_WEBHOOK_SECRET-- the same value asGITHUB_WEBHOOK_SECRETin config.env
The webhook events to register: issues, pull_request, issue_comment, pull_request_review, pull_request_review_comment.
The agent uses a fork-based workflow:
origin = YOUR_USERNAME/frameworks (fork, push branches here)
upstream = security-alliance/frameworks (official, PRs and issues here)
- NEVER push to upstream. Always push branches to origin, then open PRs.
- ALL commits must be GPG-signed (
git commit -S). - PRs are created with:
gh pr create --repo security-alliance/frameworks --head USERNAME:BRANCH - The
developbranch is the default base for all PRs.
The relay tells Hermes to load these skills for every spawn:
- frameworks-reactive-github -- core reactive agent procedures (event handling, behavior scopes, self-review policy)
- github-auth -- GitHub authentication setup (git credential handling)
- github-issues -- Create, manage, triage, and close GitHub issues
- github-pr-workflow -- Full pull request lifecycle (branch, commit, push, PR)
- github-code-review -- Review code changes, leave inline comments, submit reviews
These are bundled with Hermes. If you're adapting this setup for a different repository, you can change the --skills argument in the spawn_hermes() function in relay.py and the rescue agent's skill list in spawn_rescue().
After setup, edit ~/.hermes/profiles/frameworks/SOUL.md to adjust the agent's behavior, tone, or rules. The template uses {{BOT_USERNAME}} and {{ALLOWED_SENDERS}} placeholders that setup.sh fills in, but you can change anything after the fact.
Key sections in SOUL.md:
- IDENTITY -- the bot's GitHub account and repository
- CORE RULES -- reactive-only, whitelist, self-ignore, mandatory prefix, concise
- BEHAVIOR SCOPES -- what events the agent handles and how
- SKILLS -- which Hermes skills to load
systemctl --user status frameworks-gh-relaysystemctl --user status cloudflared-frameworks# Live tail
tail -f ~/ops/frameworks-gh-relay/relay.log
# Recent activity
tail -100 ~/ops/frameworks-gh-relay/relay.logEvery Hermes spawn writes to ~/ops/frameworks-gh-relay/spawns/:
spawns/
20260420_185700_issue_assigned_prompt.txt # Prompt sent to Hermes
20260420_185700_issue_assigned_output.log # Full PTY output
20260420_190302_rescue_20260420_185700_...log # Rescue agent output (if spawned)
Spawn files are named: TIMESTAMP_SCOPE_prompt.txt and TIMESTAMP_SCOPE_output.log.
cat ~/ops/frameworks-gh-relay/dangerous_cmds.logEach line shows: timestamp, spawn ID, and the denied command.
sqlite3 ~/ops/frameworks-gh-relay/deliveries.db "SELECT * FROM deliveries ORDER BY ts DESC LIMIT 20;"systemctl --user restart frameworks-gh-relayAlways restart after modifying relay.py or config.env.
Relay not receiving webhooks:
- Check the tunnel is running:
systemctl --user status cloudflared-frameworks - Check the tunnel URL matches what's registered in the GitHub webhook settings
- Verify the webhook secret in config.env matches the GitHub webhook secret
Agent not responding to events:
- Check the relay log for "not whitelisted" or "self-event" messages
- Verify
ALLOWED_SENDERSincludes the sender's username (case-insensitive) - Verify
BOT_USERNAMEmatches the bot's actual GitHub username - Check the spawn output logs for errors
Triple / duplicate reviews: This should no longer happen with the three-layer dedup protection. If it does, check:
- The PTY log for "DUPLICATE ... detected -- killing spawn" (layer 3)
- The rescue agent's prompt for review dedup instructions (layer 2)
- The agent's prompt for "Before submitting, CHECK for existing reviews" (layer 1)
Agent stuck / not producing output:
- Check
STUCK_TIMEOUT(default 180s) -- some complex reviews may take longer - Check the rescue agent's spawn output log
- The relay kills stuck spawns automatically and dispatches a rescue agent
GPG signing fails:
- Verify the key exists:
gpg --list-secret-keys FINGERPRINT - Verify no passphrase:
gpg --sign --detach-sign --armor -u FINGERPRINT /dev/null - Verify git config:
git config --local user.signingkeyin the frameworks repo
Hermes not found:
- Verify
hermesis in PATH or setHERMES_BINin config.env to the full path - Check:
which hermesorhermes --version
This setup is specific to security-alliance/frameworks but the architecture is general. To adapt it:
- Change ALLOWED_REPO in config.env to your target repository
- Update SOUL.md.template -- change the repository name, default branch, and behavior scopes
- Update the fork workflow in the relay's
build_prompt()function -- changesecurity-alliance/frameworksreferences and the PR creation command - Update test_relay.sh -- change the repository name in test payloads
- Register a new webhook on the target repository
- Adjust the skills list in
spawn_hermes()andspawn_rescue()if your use case differs
The core relay logic (signature verification, dedup, whitelist, stuck detection, dangerous command deny, rescue, dedup guard) is repository-agnostic and does not need changes.
Same as security-alliance/frameworks.