End-to-end instructions for deploying the agent infrastructure in your own AWS account.
Tip: If you have Claude Code installed, run
/setupin this repo for an interactive guided walkthrough. Claude will help you configure repos.json, create AWS resources, set up integrations, and deploy — step by step.
- AWS account with permissions to create EC2, S3, IAM, Secrets Manager resources
- Domain name with DNS managed by Cloudflare (for webhook tunnel)
- Linear workspace with admin access (to create OAuth app and webhook)
- GitHub organization with the repositories the agent will work on
- Claude Code OAuth token (from claude.ai/code) or Anthropic API key
- Node.js 24+ and pnpm installed locally (for building)
git clone https://github.com/your-org/hermes-swe.git
cd hermes-swe
pnpm install
pnpm buildEdit repos.json to define which repositories the agent can work on. See the Repository Configuration guide for full details.
Minimal example — a single repo using the shared default AMI:
{
"your-org/your-app": {
"amiName": "default",
"workspaceDir": "/workspace/your-app",
"bakeAmi": false,
"secrets": [],
"instanceType": "m6i.xlarge"
}
}Run the automated setup script:
bash scripts/aws-setup.sh [region]
# Defaults to eu-north-1This creates:
| Resource | Name | Purpose |
|---|---|---|
| Security Group | hermes-orchestrator-sg |
Orchestrator EC2 (SSH inbound, agent callback inbound) |
| Security Group | hermes-agent-sg |
Agent EC2 (SSH + port 3000 from orchestrator only) |
| Security Group | hermes-bake-sg |
Temporary SG for AMI baking (SSH open) |
| S3 Bucket | hermes-sessions-{your-account} |
Session state and artifacts |
| Secrets Manager | hermes/secrets |
All credentials (empty initially) |
| IAM Role | hermes-orchestrator |
EC2 permissions (S3, Secrets Manager, EC2 lifecycle) |
| SSH Key Pair | hermes-key |
SSH access to all instances |
The script outputs copy-pasteable commands for the next steps.
Set up each integration by following its dedicated guide:
- Linear Integration (required) — OAuth app, webhook, secrets
- GitHub Authentication (required) — GitHub App or PAT
- Cloudflare Setup (required) — Tunnel for webhook ingress
- Slack Notifications (optional) — Bot for session DMs
After setting up integrations, store all credentials in AWS Secrets Manager. The secret is a single JSON object stored under the name configured by aws-setup.sh (default: hermes/secrets).
aws secretsmanager put-secret-value \
--secret-id hermes/secrets \
--secret-string '{
"LINEAR_WEBHOOK_SECRET": "your-webhook-secret",
"LINEAR_CLIENT_ID": "your-client-id",
"LINEAR_CLIENT_SECRET": "your-client-secret",
"CLAUDE_CODE_OAUTH_TOKEN": "your-claude-token",
"GITHUB_APP_CLIENT_ID": "your-app-id",
"GITHUB_APP_PRIVATE_KEY": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
"GITHUB_APP_INSTALLATION_ID": "12345678",
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
"CLOUDFLARE_API_TOKEN": "your-api-token",
"CLOUDFLARE_ACCOUNT_ID": "your-account-id",
"CLOUDFLARE_ZONE_ID": "your-zone-id",
"SENTRY_ACCESS_TOKEN": "optional-sentry-token",
"METABASE_API_KEY": "optional-metabase-key"
}'Required secrets:
| Key | Source | Purpose |
|---|---|---|
LINEAR_WEBHOOK_SECRET |
Linear setup | Verify webhook signatures |
LINEAR_CLIENT_ID |
Linear setup | OAuth app |
LINEAR_CLIENT_SECRET |
Linear setup | OAuth app |
CLAUDE_CODE_OAUTH_TOKEN |
Claude Code CLI | Agent authentication (or use ANTHROPIC_API_KEY) |
ANTHROPIC_API_KEY |
Anthropic API | Agent authentication (alternative to OAuth token) |
Choose one GitHub auth method:
| Key | Source | Purpose |
|---|---|---|
GITHUB_APP_CLIENT_ID |
GitHub setup | GitHub App (recommended) |
GITHUB_APP_PRIVATE_KEY |
GitHub setup | GitHub App (recommended) |
GITHUB_APP_INSTALLATION_ID |
GitHub setup | GitHub App (recommended) |
| — or — | ||
GITHUB_TOKEN |
GitHub setup | Personal Access Token |
Optional secrets:
| Key | Source | Purpose |
|---|---|---|
SLACK_BOT_TOKEN |
Slack setup | Session notifications |
CLOUDFLARE_API_TOKEN |
Cloudflare setup | Preview tunnel creation |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare setup | Preview tunnel creation |
CLOUDFLARE_ZONE_ID |
Cloudflare setup | Preview tunnel creation |
SENTRY_ACCESS_TOKEN |
Sentry dashboard | Sentry MCP server |
METABASE_API_KEY |
Metabase admin | Metabase MCP server |
Launch a small EC2 instance for the orchestrator:
aws ec2 run-instances \
--image-id <ubuntu-24.04-arm64-ami> \
--instance-type t4g.nano \
--key-name hermes-key \
--security-group-ids <hermes-orchestrator-sg-id> \
--subnet-id <your-subnet-id> \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=hermes-orchestrator},{Key=Project,Value=hermes}]' \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":20,"VolumeType":"gp3"}}]'The
aws-setup.shscript outputs this command with the correct AMI and resource IDs for your region.
Get the public IP:
aws ec2 describe-instances --filters "Name=tag:Name,Values=hermes-orchestrator" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output textBefore running the setup script, copy your Cloudflare Tunnel credentials to the orchestrator. See Cloudflare Setup for detailed instructions.
# Copy tunnel credentials
scp -i ~/.ssh/hermes-key.pem ~/.cloudflared/<tunnel-id>.json \
ubuntu@<orchestrator-ip>:/tmp/cloudflared-credentials.jsonssh -i ~/.ssh/hermes-key.pem ubuntu@<orchestrator-ip> \
"GITHUB_TOKEN=ghp_xxx bash -s" < scripts/setup-orchestrator.shThis script:
- Installs Node.js 24, pnpm, AWS CLI, cloudflared
- Clones this repo to
/opt/agent/hermes-swe - Builds the orchestrator
- Auto-detects VPC networking (private IP, subnet, security groups)
- Writes
/opt/agent/envwith all configuration - Configures Cloudflare Tunnel as a systemd service
- Configures the orchestrator as a systemd service
SSH into the orchestrator and verify /opt/agent/env:
ssh -i ~/.ssh/hermes-key.pem ubuntu@<orchestrator-ip>
cat /opt/agent/envKey environment variables:
# Auto-detected by setup script
CALLBACK_BASE_URL=http://<private-ip>:3002
AWS_REGION=eu-north-1
SUBNET_ID=subnet-xxx
AGENT_SECURITY_GROUP_ID=sg-xxx
KEY_NAME=hermes-key
SESSIONS_BUCKET=hermes-sessions-xxx
SECRET_NAME=hermes/secrets
# Set manually
LINEAR_REDIRECT_URI=https://your-domain.example.com/oauth/callback
PREVIEW_DOMAIN=your-domain.example.com # Optional, for preview URLs
SLACK_CHANNEL_ID=C0123456789 # Optional, fallback Slack channel
DRY_RUN=false
# AMI IDs (set after baking in Step 10)
AGENT_AMI_ID_DEFAULT=ami-xxx
AGENT_AMI_ID_APP=ami-xxxFrom the orchestrator, bake the agent AMI:
# Generic AMI (for repos with bakeAmi: false)
sudo -u ubuntu bash /opt/agent/hermes-swe/scripts/bake-ami.sh
# Repo-specific AMI (for repos with bakeAmi: true)
sudo -u ubuntu bash /opt/agent/hermes-swe/scripts/bake-ami.sh --repo your-org/your-appThis launches a temporary EC2, installs all dependencies (Docker, Node.js, pnpm, Claude Code), clones repos, runs builds, creates an AMI, and updates /opt/agent/env with the new AMI ID.
The process takes 10-20 minutes depending on your repo's build time.
sudo systemctl start orchestrator
sudo systemctl status orchestrator
# Check health
curl -s http://localhost:3001/health | jqVisit the OAuth install endpoint in your browser:
https://your-domain.example.com/oauth/install
This redirects to Linear's authorization page. Approve access for your workspace.
- In Linear, assign a ticket to the agent (the agent appears as an assignable member after OAuth install)
- Watch the orchestrator logs:
sudo journalctl -u orchestrator -f - The orchestrator should launch an EC2 instance, wait for it to boot, and send the ticket to the agent
- Progress should appear as activity on the Linear ticket
- On completion, a PR should be created and the EC2 terminated
# From your local machine
bash scripts/deploy-orchestrator.sh ubuntu@<orchestrator-ip>
# Or from a non-main branch
bash scripts/deploy-orchestrator.sh ubuntu@<orchestrator-ip> my-branchRe-bake when dependencies change (new packages, Docker images, etc.):
# SSH into orchestrator
bash /opt/agent/hermes-swe/scripts/bake-ami.sh --repo your-org/your-app
# Restart orchestrator to use the new AMI
sudo systemctl restart orchestratorSee Repository Configuration for how to add repos, configure AMIs, set up previews, and add repo-specific scripts.
Skills live in skills/ and are copied to every agent at boot (by init-instance.sh). No AMI re-bake needed — just deploy the code change to the orchestrator and new agents will pick up the updated skills.
- Create
skills/your-skill/SKILL.md - Follow the format: YAML frontmatter (
name,description) + markdown body with instructions - Reference it from the system prompt in
agent-service/prompts/system.md - Deploy:
bash scripts/deploy-orchestrator.sh ubuntu@<orchestrator-ip>
The agent firewall blocks all outbound traffic except approved domains. Firewall rules are applied at boot time (by init-instance.sh), not baked into the AMI — so changes take effect on the next agent session without re-baking.
- Base domains:
ami/allowed-domains.txt(shared across all repos) - Repo-specific domains:
ami/<scriptsDir>/allowed-domains.txt(appended at boot)
Add domains one per line (comments with #). Deploy the code change to the orchestrator.
- Add the MCP server configuration to your repo's
.mcp.jsonat the repository root - If the server needs secrets (API tokens, credentials): add the key names to the
secretsarray inrepos.jsonand the actual values to AWS Secrets Manager. These are written to/opt/agent/envand available as environment variables to MCP servers. - If the server needs non-secret config (URLs, project IDs): create a
.claude/.env.examplein your repo with the values, setenvExampleinrepos.json, and it gets copied to.env.localat boot.
See Repository Configuration — MCP Server Configuration for details.