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.
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.
Prerequisites: a Kubernetes cluster and Helm v3.
helm upgrade hermes-agent-operator oci://ghcr.io/hermeum/charts/hermes-agent-operator \
--install --namespace hermes-agent --create-namespacekubectl create secret generic my-hermes-secret \
--from-literal=ANTHROPIC_API_KEY=sk-ant-...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
EOFkubectl get hermesagent my-agent
kubectl get pods -l app.kubernetes.io/instance=my-agentThe 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-operatorThen run the /hermes-agent-operator skill to create a custom resource.
hermes.confighermes.storagehermes.workspacehermes.packageshermes.pluginshermes.skillshermes.cronshermes.bundleshermes.env/hermes.envFromhermes.resourceshermes.imagehermes.initChownDatahermes.initScriptshermes.profilessearxngcamofoxsecurity.rbacsecurity.networkPolicynetworking.servicenetworking.ingresssuspendhostUserspodAnnotationspodLabelspriorityClassNameruntimeClassName
Configure the Hermes agent runtime. raw, apiServer, and webhook can be used independently or together.
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-6Enable 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 emptyTo 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-...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>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` belowMount 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.
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 aSnapshotUnsupportedcondition 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 defaultRetained 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-agentRestore back into the agent with persistence.existingSnapshot.
Seed files and secrets into the agent's home directory before startup.
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
...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-keysPrecedence on key collision (last-wins):
configMapRefsentries, in list ordersecretRefsentries, in list order
Secrets override ConfigMaps, and later entries override earlier ones.
Note: The singular
configMapRef/secretRefforms 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.
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.
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.
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"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)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 verdictSchedule 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 underDefine 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 nameInject 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-configCPU 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: 1GiOverride 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:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefA 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
imageto be a plain string:hermes: image: repository: nousresearch/hermes-agent tag: latestPrefer the string form; it also works with kustomize image overrides.
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
hermescommand inside the container will also change the ownership of files under/opt/datato the hermes user (10000:10000).
hermes:
initChownData: true # optional; defaults to falseRun 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/ghUse spec.initContainers instead when you need a different image, custom volume mounts, or fine-grained resource limits on the init step.
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.dotEnvon 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.
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"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"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"]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: TCPService 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 | SCTPOptional 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-tlsPause the agent by scaling its StatefulSet to 0 without deleting the resource or its data.
suspend: true # optional; defaults to falseRun 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 namespaceOmitting 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.
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 restartAdd 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.
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 globalDefaultThe 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.
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 runtimeThe 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.
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=falseor run the manager with --heartbeat-disabled.
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).
Join our Discord to ask questions and share feedback.
See CONTRIBUTING.md.
Copyright 2026. Licensed under the MIT License.
