hermeum/hermes-agent-operator

Kubernetes operator for managing Hermes agent instances in declarative manner.

★ 39Forks 4GoGitHub ↗Compare
hermes-agenthermes-agentshermeumkubernetesoperator

README

hermes-agent-operator

Hermes Gopher

Self-hosting Hermes agent on Kubernetes in a declarative, reproducible manner.

Note: If you need a platform to manage Hermes agents for your team, check out Hermeum — it's built on this operator and adds a dashboard, templates, and shared credentials.

Why

Hermes agent is a powerful tool for automating tasks — but it is designed for personal use. Running it across a team is difficult: configurations drift, skills go out of sync, and there is no shared source of truth for what each agent does or what it has access to.

hermes-agent-operator solves this by managing Hermes agents as Kubernetes custom resources. You declare the full state of an agent — its config, workspace files, skills, crons, and bundles — in a single manifest. The operator keeps the running agent in sync with that declaration.

Quick Start

Prerequisites: a Kubernetes cluster and Helm v3.

1. Install the operator

helm upgrade hermes-agent-operator oci://ghcr.io/hermeum/charts/hermes-agent-operator \
  --install --namespace hermes-agent --create-namespace

2. Create a secret with your API key

kubectl create secret generic my-hermes-secret \
  --from-literal=ANTHROPIC_API_KEY=sk-ant-...

3. Deploy a HermesAgent

kubectl apply -f - <<EOF
apiVersion: agents.hermeum.app/v1alpha1
kind: HermesAgent
metadata:
  name: my-agent
spec:
  hermes:
    config:
      raw:
        model:
          provider: anthropic
          default: claude-sonnet-4-6
    envFrom:
      - secretRef:
          name: my-hermes-secret
EOF

4. Verify

kubectl get hermesagent my-agent
kubectl get pods -l app.kubernetes.io/instance=my-agent

Create a HermesAgent with the Skill

The hermes-agent-operator skill lets a running Hermes agent scaffold and apply HermesAgent manifests on your behalf. Install it once into your Hermes agent:

hermes skills install hermeum/hermes-agent-operator/skills/hermes-agent-operator

Then run the /hermes-agent-operator skill to create a custom resource.

Configuration

hermes.config

Configure the Hermes agent runtime. raw, apiServer, and webhook can be used independently or together.

raw

Pass a verbatim config.yml as free-form YAML. Anything valid in a Hermes config file is accepted here.

hermes:
  config:
    raw:                           # optional; omit if no custom config.yml is needed
      model:
        provider: anthropic
        default: claude-sonnet-4-6

apiServer

Enable the built-in gateway API. The operator always generates a Kubernetes Secret named <agent-name>-hermes containing a random API_SERVER_KEY. When enabled: true, the operator sets API_SERVER_ENABLED=true, API_SERVER_PORT, and injects the key into the agent container automatically.

hermes:
  config:
    apiServer:                     # optional; omit to disable the gateway API
      enabled: true
      port: 8642                   # optional; defaults to 8642. 
      corsOrigins:                 # optional; browser origins allowed to call the API server
        - https://app.example.com  # (sets API_SERVER_CORS_ORIGINS). CORS stays disabled when empty

To use your own API key instead of the operator-generated one, set API_SERVER_KEY via hermes.workspace.dotEnv (your value overrides the operator-generated one):

hermes:
  workspace:
    dotEnv:
      secretRef:
        name: my-api-server-key     # Secret with key API_SERVER_KEY=sk-...

webhook

Enable the webhook ingress. When enabled: true, the operator sets WEBHOOK_ENABLED=true and injects a WEBHOOK_SECRET (the HMAC secret) into the agent container. By default the secret is generated once and stored in the operator-managed <agent-name>-hermes Secret, then preserved across reconciles so it is not rotated.

hermes:
  config:
    webhook:                       # optional; omit to disable the webhook ingress
      enabled: true
      port: 8644                   # optional; defaults to 8644. 

To use your own HMAC secret instead of the operator-generated one — e.g. to share a known value with external webhook senders, or to set INSECURE_NO_AUTH for testing — set WEBHOOK_SECRET via hermes.workspace.dotEnv (your value overrides the operator-generated one):

hermes:
  workspace:
    dotEnv:
      secretRef:
        name: my-webhook-secret    # Secret with key WEBHOOK_SECRET=<your-hmac-value>

hermes.storage

persistence

Persistent volume for agent data at /opt/data. Without persistence, data is lost on pod restart.

hermes:
  storage:
    persistence:
      enabled: true
      size: 10Gi                   # optional; defaults to 10Gi
      storageClassName: standard   # optional; omit to use the cluster default StorageClass
      existingClaim: my-pvc        # optional; omit to provision a new PVC automatically
      existingSnapshot: my-snap    # optional; see `persistence.existingSnapshot` below

Mount the agent data volume as a PVC restored from a VolumeSnapshot, instead of provisioning a new empty PVC.

hermes:
  storage:
    persistence:
      existingSnapshot: hermes-data-my-agent-0-20260909030000  # must exist in this namespace and be ReadyToUse
      storageClassName: standard   # optional; selects the restored PVC's class (omit for cluster default)

The snapshot must be ReadyToUse; otherwise a RestoreFailed condition is surfaced and the agent is left untouched. The operator provisions <snapshot>-restore (sized from the snapshot's restoreSize; enabled/size are ignored) while the agent keeps running, then deletes the StatefulSet once (its volumeClaimTemplate is immutable) so the restored volume mounts. Later changes — pointing at another snapshot, or reverting to an earlier one — are plain rolling updates. Snapshots are never deleted; the previous volume is left for you to clean up.

snapshot

Periodic CSI volume snapshots of the agent data PVC — the only state in the deployment that cannot be reproduced (sessions, skills, memories, .env history).

Prerequisites: your StorageClass must be backed by a CSI driver with snapshot support, and the cluster must run the snapshot-controller (VolumeSnapshot CRDs + controller from kubernetes-csi/external-snapshotter). Without it, the operator surfaces a SnapshotUnsupported condition instead of failing silently.

hermes:
  storage:
    snapshot:                      # optional; omit to disable snapshots
      enabled: true
      schedule: "0 3 * * *"        # required when enabled; standard cron expression
      retention: 3                 # optional; keep newest N snapshots, defaults to 3
      volumeSnapshotClassName: my-class  # optional; omit to use the cluster default

Retained snapshots are listed in status.snapshot:

kubectl get hermesagent my-agent -o jsonpath='{.status.snapshot}'

To list the VolumeSnapshot objects of an agent directly:

kubectl get volumesnapshots -l agents.hermeum.app/agent=my-agent

Restore back into the agent with persistence.existingSnapshot.

hermes.workspace

Seed files and secrets into the agent's home directory before startup.

files

Seed files into the agent's home directory. Keys are relative paths; use / as a separator for subdirectories.

hermes:
  workspace:
    files:                         # optional; omit if no files need to be seeded
      SOUL.md: |
        You are a pragmatic senior engineer.
      skills/custom/SKILL.md: |   # subdirectory path — operator creates parent dirs automatically
        # My Custom Skill
        ...

dotEnv

Generate a $HERMES_HOME/.env file from Kubernetes ConfigMaps and/or Secrets. Each key in the referenced object(s) becomes a KEY=VALUE line in the file. Useful for tools that read .env files at startup.

At least one of configMapRefs/secretRefs must be set; both may be set at once, and each list may contain multiple entries (up to 64 per list).

hermes:
  workspace:
    dotEnv:                        # optional; omit if no .env file is needed
      configMapRefs:               # non-secret values
        - name: my-env-configmap
        - name: my-other-configmap
      secretRefs:                  # secret values
        - name: my-env-secret
        - name: my-api-keys

Precedence on key collision (last-wins):

  1. configMapRefs entries, in list order
  2. secretRefs entries, in list order

Secrets override ConfigMaps, and later entries override earlier ones.

Note: The singular configMapRef/secretRef forms are still accepted for compatibility but are deprecated; they will be removed in the v1 type. They take the lowest precedence: ConfigMap first, then Secret, then the plural lists above.

hermes.packages

Pre-install language packages before the agent starts. Each sub-key corresponds to a package manager (pip, npm). Packages are installed via init containers and stored on the persistent volume, so they survive pod restarts.

Removing a package from any list wipes and reinstalls the remaining set on the next reconcile, so the installed state always matches the declaration.

pip

Installs Python packages via uv pip install. install entries are pip specifiers — bare name, version-pinned, or extras. extraArgs are appended verbatim to the uv pip install command, useful for custom index URLs or other flags.

Packages are installed into $HERMES_HOME/.python-packages and made available via PYTHONPATH.

hermes:
  packages:
    pip:                           # optional
      install:                     # pip specifiers to install
        - requests
        - pandas==2.1.0
        - "beautifulsoup4[lxml]"
      extraArgs:                   # optional; extra flags passed to `uv pip install`
        - "--index-url=https://my-private-index.example.com/simple"
        - "--extra-index-url=https://pypi.org/simple"

Installed binaries land in $HERMES_HOME/.python-packages/bin. To add them to PATH for shell-based tools, seed a .bashrc via hermes.workspace:

hermes:
  packages:
    pip:
      install:
        - requests
  workspace:
    files:
      home/.bashrc: |
        export PATH="$HERMES_HOME/.python-packages/bin:$PATH"

Note: These packages are available to Python code run by the agent (tool execution, scripts, etc.). They do not affect the Hermes agent process itself, which uses its own virtual environment at /opt/hermes/.venv.

npm

Installs npm packages via npm install -g --prefix. install entries are standard npm package specifiers — bare name, scoped (@scope/name), or version-pinned (pkg@^1.0.0).

Packages are installed into $HERMES_HOME/.npm-packages and made available via NODE_PATH.

hermes:
  packages:
    npm:                           # optional
      install:                     # npm package specifiers to install
        - "@anthropic-ai/sdk"
        - "@anthropic-ai/mcp-server-puppeteer"
        - "typescript@^5.0.0"

Installed binaries land in $HERMES_HOME/.npm-packages/bin. To add them to PATH for shell-based tools, seed a .bashrc via hermes.workspace:

hermes:
  packages:
    npm:
      install:
        - typescript
  workspace:
    files:
      home/.bashrc: |
        export PATH="$HERMES_HOME/.npm-packages/bin:$PATH"

hermes.plugins

Install Hermes plugins at startup. Use owner/repo shorthand or a full Git URL.

hermes:
  plugins:                         # optional; omit if no plugins are needed
    - identifier: hermes-agent/plugin-stocks  # required; owner/repo or full Git URL
      enable: true                 # optional; defaults to true (auto-enable after install)

hermes.skills

Install skills via hermes skills install. Skills are reconciled on every pod start.

hermes:
  skills:                          # optional; omit if no skills are needed
    - identifier: official/finance/stocks  # required; skill path or HTTP(S) URL to a SKILL.md
      category: finance            # optional; category folder to install into
      name: stocks                 # optional; overrides the skill name from SKILL.md frontmatter
      force: false                 # optional; set true to install despite a blocked scan verdict

hermes.crons

Schedule recurring prompts. Supported formats: every Xh, every Xm, or standard cron expressions.

hermes:
  crons:                           # optional; omit if no scheduled jobs are needed
    - name: daily-standup          # required; human-friendly name and reconciliation key
      schedule: every 24h          # required; e.g. "every 2h", "30m", "0 9 * * *"
      prompt: Summarize yesterday's activity and suggest today's priorities  # optional
      deliver: slack               # optional; origin | local | telegram | discord | signal | platform:chat_id
      repeat: 1                    # optional; number of times to repeat the job
      skills:                      # optional; skills to attach to this job
        - stocks
      script: standup.sh           # optional; path under ~/.hermes/scripts/
      noAgent: false               # optional; true to run script and deliver stdout directly, skipping the LLM
      workdir: /home/hermes        # optional; absolute working directory for the job
      monitorScript: source.sh     # optional; monitor mode: script under ~/.hermes/scripts/ run each tick BEFORE the agent; unchanged output suppresses the agent run (mutually exclusive with monitorURL)
      monitorURL: https://example.com/status  # optional; monitor mode: http(s) URL fetched each tick instead of a script (mutually exclusive with monitorScript)
      model: gpt-5                 # optional; pin this job to a specific inference model
      provider: openrouter         # optional; inference provider paired with model (e.g. 'openrouter', 'nous')
      reasoningEffort: high        # optional; none, minimal, low, medium, high, xhigh, max, or ultra
      continuity: false            # optional; true to inject the job's previous output into each run's prompt
      profile: default             # optional; Hermes profile name to run under

hermes.bundles

Define slash-command bundles that group related skills under a single name.

hermes:
  bundles:                         # optional; omit if no bundles are needed
    - name: finance                # required; becomes the /slash command name
      description: Finance helpers # optional; shown in /help and bundle list
      skills:                      # optional; skill names to include
        - stocks
      instruction: Use these tools for financial queries  # optional; prepended to skill content
      force: false                 # optional; set true to overwrite an existing bundle with the same name

hermes.env / hermes.envFrom

Inject environment variables directly or from existing ConfigMaps and Secrets.

hermes:
  env:                             # optional; omit if no direct env vars are needed
    - name: TZ
      value: UTC
  envFrom:                         # optional; omit if no ConfigMap/Secret injection is needed
    - secretRef:
        name: my-api-keys
    - configMapRef:
        name: my-agent-config

hermes.resources

CPU and memory for the agent container. Defaults: limits 2 CPU / 4Gi, requests 500m / 1Gi.

hermes:
  resources:                       # optional; omit to use defaults
    limits:
      cpu: "2"
      memory: 4Gi
    requests:
      cpu: 500m
      memory: 1Gi

hermes.image

Override the agent image used for the hermes-agent container and all init containers. Defaults to nousresearch/hermes-agent:latest.

The image is a full image reference string, so it accepts a tag:

hermes:
  image: nousresearch/hermes-agent:v1.2.3    # optional; omit to use the default image

...or a digest, for an immutable reference:

hermes:
  image: nousresearch/hermes-agent@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

A reference without a tag or digest (e.g. nousresearch/hermes-agent) uses the latest tag per Kubernetes image semantics.

Backward compatibility: the deprecated object form is still accepted, but the operator logs a warning on each reconciliation, and the next API version will require image to be a plain string:

hermes:
  image:
    repository: nousresearch/hermes-agent
    tag: latest

Prefer the string form; it also works with kustomize image overrides.

hermes.initChownData

Run an init container that sets /opt/data ownership to the hermes user (10000:10000). Useful when using an existing PVC whose data was written by a different user.

Note: Because the container starts as root (see FAQ), running the hermes command inside the container will also change the ownership of files under /opt/data to the hermes user (10000:10000).

hermes:
  initChownData: true              # optional; defaults to false

hermes.initScripts

Run shell scripts in init containers before the agent starts. The operator automatically uses the hermes-agent image and wires up the environment variables (HERMES_HOME, HOME, and any hermes.env / hermes.envFrom), volume mounts, and security context.

Scripts run after all operator-managed init containers.

hermes:
  initScripts:                       # optional; omit if no custom init scripts are needed
    - name: install-gh               # required; must be unique within the pod
      script: |                      # required; executed via /bin/sh -ec
        mkdir -p $HERMES_HOME/.local/bin
        GH_VERSION=2.94.0
        curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" \
          | tar -xz -C /tmp
        mv "/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" $HERMES_HOME/.local/bin/gh
        chmod +x $HERMES_HOME/.local/bin/gh

Use spec.initContainers instead when you need a different image, custom volume mounts, or fine-grained resource limits on the init step.

hermes.profiles

Declare named Hermes profiles that are created and configured alongside the default profile. Each profile supports its own config, workspace files, .env, plugins, skills, bundles, and crons. All profiles share a single multiplexed gateway.

hermes:
  profiles:
    coder:                             # profile name; key becomes the profile identifier
      clone: true                      # optional; clone config/.env/SOUL.md/skills from default at creation
      config:
        raw:                           # optional; raw config.yaml content (JSON-serialized)
          model: claude-sonnet-4-5
          # NOTE: apiServer and webhook are not supported here
      workspace:
        dotEnv:                        # optional; write .env from a ConfigMap and/or Secret
          secretRef:
            name: coder-env-secret
        files:                         # optional; workspace files copied to the profile home dir
          SOUL.md: |
            You are a senior software engineer...
      plugins:                         # optional; same schema as hermes.plugins
        - identifier: owner/hermes-plugin-github
      skills:                          # optional; same schema as hermes.skills
        - identifier: https://example.com/skills/code-review.md
          name: code-review
      bundles:                         # optional; same schema as hermes.bundles
        - name: engineering
          skills: [code-review]
      crons:                           # optional; same schema as hermes.crons
        - name: daily-standup
          schedule: "0 9 * * 1-5"
          prompt: "Summarise yesterday's commits"

Tip: Use workspace.dotEnv on each profile to load environment variables from a ConfigMap and/or Secret. This is the recommended way to isolate credentials and configuration (e.g. API keys, tokens) per profile without leaking them across profiles.

searxng

Optional sidecar that runs a local SearXNG instance, enabling the agent's web_search tool without an external API key. When enabled, the operator automatically injects SEARXNG_URL into the agent container and sets web.search_backend: "searxng" in the generated Hermes config (unless already set in hermes.config.raw):

web:
  search_backend: "searxng"
searxng:
  enabled: true                    # defaults to false; omit the entire block to disable
  image: searxng/searxng:latest    # optional; omit to use the default image (searxng/searxng:latest)
  resources:                       # optional; omit to use no resource constraints
    limits:
      cpu: 500m
      memory: 512Mi
    requests:
      cpu: 100m
      memory: 128Mi
  configFiles:                     # optional; files mounted at /etc/searxng
    settings.yml: |                # omit to use the operator default (enables JSON response format)
      use_default_settings: true
      search:
        formats:
          - html
          - json
  persistence:                     # optional; omit to use an emptyDir (state lost on restart)
    enabled: true
    size: 1Gi                      # optional; defaults to 1Gi
    storageClassName: standard     # optional; omit to use the cluster default StorageClass
    existingClaim: my-searxng-pvc  # optional; omit to provision a new PVC automatically
  env:                             # optional; additional env vars for the SearXNG container
    - name: UWSGI_WORKERS
      value: "4"

camofox

Optional sidecar for browser automation via Camofox. When enabled, CAMOFOX_URL is automatically injected into the agent container.

camofox:
  enabled: true                    # defaults to false; omit the entire block to disable
  image: ghcr.io/jo-inc/camofox-browser:latest    # optional; omit to use the default image
  resources:                       # optional; omit to use no resource constraints
    limits:
      cpu: "1"
      memory: 1Gi
    requests:
      cpu: 200m
      memory: 256Mi
  persistence:                     # optional; omit to use an emptyDir (browser state lost on restart)
    enabled: true
    size: 1Gi                      # optional; defaults to 1Gi
    storageClassName: standard     # optional; omit to use the cluster default StorageClass
    existingClaim: my-camofox-pvc  # optional; omit to provision a new PVC automatically
  env:                             # optional; additional env vars for the Camofox container
    - name: DISPLAY
      value: ":99"

security.rbac

ServiceAccount and Role configuration. A ServiceAccount is created by default.

security:
  rbac:                            # optional; omit to skip RBAC resource creation
    createServiceAccount: true     # optional; defaults to true
    serviceAccountName: my-sa      # optional; used only when createServiceAccount is false
    serviceAccountAnnotations:     # optional; use for cloud provider identity (AWS IRSA, GCP Workload Identity)
      eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/my-role
    additionalRules:               # optional; extra rules appended to the generated Role
      - apiGroups: [""]
        resources: ["secrets"]
        verbs: ["get", "list"]

security.networkPolicy

NetworkPolicy configuration. Created only when this block is present.

Ingress is allowed on the API server port (hermes.config.apiServer.port), the webhook port (hermes.config.webhook.port), and any additional container ports declared in hermes.ports. When a NetworkPolicy is enabled, declare every container port you want reachable in hermes.ports — otherwise the default-deny policy will block traffic to it, even if a Service routes to it.

security:
  networkPolicy:                   # optional; omit the entire block to skip NetworkPolicy creation
    enabled: true                  # optional; defaults to true when the block is present
    allowedIngressCIDRs:           # optional; CIDRs allowed to reach this agent
      - 10.0.0.0/8
    allowedIngressNamespaces:      # optional; namespaces allowed to reach this agent
      - my-namespace
    allowedEgressCIDRs:            # optional; CIDRs this agent can reach (default allows 443 for AI APIs)
      - 0.0.0.0/0
    allowDNS: true                 # optional; defaults to true (allows port 53)
    additionalEgress:              # optional; custom egress rules beyond DNS + HTTPS defaults
      - ports:
          - port: 5432
            protocol: TCP

networking.service

Service configuration for the agent. Ports defined by hermes.config.apiServer and hermes.config.webhook are automatically exposed — use ports only for additional ports beyond those.

networking:
  service:
    type: ClusterIP                # optional; ClusterIP (default) | LoadBalancer | NodePort
    annotations:                   # optional; custom annotations on the Service
      service.beta.kubernetes.io/aws-load-balancer-type: nlb
    ports:                         # optional; additional ports.
      - name: metrics              
        port: 9090
        targetPort: 9090           # optional; defaults to port
        protocol: TCP              # optional; TCP (default) | UDP | SCTP

networking.ingress

Optional Ingress for exposing the agent externally.

networking:
  ingress:
    enabled: true                  # optional; defaults to false
    className: nginx               # optional; name of the IngressClass to use
    annotations:                   # optional; custom annotations on the Ingress
      cert-manager.io/cluster-issuer: letsencrypt
    hosts:                         # optional; omit if Ingress is not needed
      - host: agent.example.com
        paths:                     # required; at least one path per host
          - path: /                # optional; defaults to /
            pathType: Prefix       # optional; Prefix (default) | Exact | ImplementationSpecific
            port: 8642             # required; backend Service port, e.g. the API server port
    tls:                           # optional; omit if TLS termination is not needed
      - hosts:
          - agent.example.com
        secretName: agent-tls

suspend

Pause the agent by scaling its StatefulSet to 0 without deleting the resource or its data.

suspend: true                      # optional; defaults to false

hostUsers

Run the agent pod in its own user namespace. The agent container runs as root, and a user namespace maps that root to an unprivileged UID on the node, so a container breakout does not land on the node as root.

hostUsers: false                   # optional; omit to share the host user namespace

Omitting the field, or setting it to true, leaves the pod in the host user namespace — the Kubernetes default. A change to this value starts a rolling restart of the StatefulSet's pods.

User namespaces are stable as of Kubernetes v1.36, where the UserNamespacesSupport feature gate is locked on and ignored; they were on by default for several releases before that. What still gates the feature is the node. The pod must land on a Linux node meeting the requirements: kernel 6.3 or newer, a CRI runtime that supports user namespaces (containerd 2.0+ or CRI-O 1.25+), and idmap-capable filesystems both for the kubelet directory and for every volume the pod mounts. Where only some nodes in a cluster qualify, schedule the agent onto one that does.

podAnnotations

Add custom annotations to the agent's pod template. Changing any key/value triggers a rolling restart of the StatefulSet's pods — set a key like rotatedAt to an RFC3339 timestamp and update it whenever a restart is needed, equivalent to kubectl rollout restart statefulset.

podAnnotations:                      # optional
  rotatedAt: "2026-07-06T12:00:00Z"  # any change here triggers a rolling restart

podLabels

Add labels to the agent's pod template. An object that selects pods by label, such as a NetworkPolicy or a PodMonitor, can then select the agent pod.

podLabels:                           # optional
  example.com/internet-client: "true"
  example.com/traefik-route: "true"

The operator-managed app.kubernetes.io/name, app.kubernetes.io/instance and app.kubernetes.io/managed-by labels are applied last and always win. An entry in podLabels cannot shadow one of them, and cannot break the StatefulSet pod selector. The labels go on the pod template only; the StatefulSet labels do not change. A change to any key starts a rolling restart.

priorityClassName

Assign the agent Pod a PriorityClass. A PriorityClass controls the Pod's scheduling order. It also controls how early the Pod is evicted under node pressure.

priorityClassName: system-cluster-critical  # optional; omit to use the cluster's globalDefault

The PriorityClass must already exist in the cluster. The operator does not create it. A change to this value starts a rolling restart of the StatefulSet's pods.

runtimeClassName

Run the agent Pod under a named RuntimeClass. This replaces the cluster's default container runtime for this Pod. The agent executes model-authored shell commands, so a sandboxed runtime such as gVisor or Kata Containers is a good fit for it.

runtimeClassName: kata-qemu          # optional; omit to use the cluster default runtime

The RuntimeClass must already exist in the cluster. The operator does not create it. A change to this value starts a rolling restart of the StatefulSet's pods.

Heartbeat

The operator sends an anonymous heartbeat via PostHog so we can count how many operator installations are running. Prometheus telemetry (reconciliation metrics) stays entirely in your cluster; the heartbeat is the only call home, and it can be disabled.

What is collected (and nothing else):

  • An anonymous deployment ID — a random UUID generated each time a controller starts. It is not persisted and contains no user, cluster, or host information. Every running controller sends its own heartbeat, so the distinct number of deployment IDs in a recent window equals the number of running installations.
  • The operator version.

What is not collected: agent names, namespaces, configuration, workspace contents, hostnames, IP addresses, or any other user-identifiable data.

To disable the heartbeat, set manager.heartbeat.enabled=false when installing:

helm upgrade hermes-agent-operator oci://ghcr.io/hermeum/charts/hermes-agent-operator \
  --install --namespace hermes-agent --create-namespace \
  --set manager.heartbeat.enabled=false

or run the manager with --heartbeat-disabled.

FAQ

Q: How are things self-installed by Hermes managed via the custom resource?

They aren't. The operator only manages what is explicitly declared in the HermesAgent custom resource. Anything Hermes installs on its own at runtime (plugins, packages, etc.) is outside the operator's control and will not be reconciled.

Q: How do I make binaries, packages, etc. persistent?

Only the HERMES_HOME path (/opt/data) is persisted across pod restarts. Anything that needs to survive a restart must be placed under HERMES_HOME. The operator sets HOME=/opt/data/home so tools that respect $HOME will write there automatically.

Q: Why does the Hermes container run as root?

The official Hermes image uses s6-overlay, which requires the process to start as root for service supervision setup. Once initialisation is complete, s6-overlay drops privileges and runs the agent as the hermes user (10000:10000).

Community

Join our Discord to ask questions and share feedback.

Contributing

See CONTRIBUTING.md.

License

Copyright 2026. Licensed under the MIT License.

Contributors

noahinghsdwilshaldoborrerohermeum-bot

Issues