Hermes Deep Dive: The Developer’s Guide - 2026-10-23

By the Smartotics DevTools Desk

There’s a particular kind of tool that quietly reshapes how a team works: not a framework you adopt in a sprint, but a substrate you start reaching for whenever a task is tedious, repetitive, or just slightly too clever for a shell one-liner. Hermes is that tool for a growing number of developers. This guide covers what Hermes actually is, how to install and configure it, the features that matter in daily work, and the advanced workflows that separate casual users from people who’ve made it part of their muscle memory.


What is Hermes?

Origin and background

Hermes began as an internal utility for automating the “glue work” that accumulates around any serious codebase—running linters across a monorepo, orchestrating multi-step release chores, scraping and reconciling data from a dozen sources, and wiring together CLIs that were never designed to talk to each other. It was open-sourced after the maintainers realized that most of their internal tooling was, at its core, the same pattern: a declarative task definition, a pluggable execution engine, and a scripting layer for the parts that resist declarativism.

The project’s name is a nod to the messenger god—and to the fact that Hermes is fundamentally about moving intent into action across boundaries: between languages, between services, between a developer’s terminal and a CI runner.

Core value proposition

Hermes is best understood as a task orchestration runtime with a scripting escape hatch. You describe what you want in a hermes.yaml (or .hermes.toml) manifest, and Hermes handles dependency resolution, parallel execution, caching, retries, and structured logging. When the declarative layer isn’t enough, you drop into embedded scripting (Python, JavaScript, or a shell dialect) without leaving the same execution context—so state, environment, and artifacts flow seamlessly between declarative and imperative steps.

Three things make it genuinely useful:

  1. A single manifest format that replaces Makefiles, npm scripts, shell scripts, and ad-hoc CI YAML for local and CI use.
  2. Content-addressed caching of task outputs, keyed on inputs, environment, and tool versions—so re-running is nearly free when nothing changed.
  3. First-class observability: every task emits structured events (JSON lines by default) that you can pipe into your existing logging stack.

What makes it different from alternatives

Make is fast but hostile to anything beyond file-based dependencies. Just and Task are friendlier but don’t handle cross-language orchestration or caching well. Bazel is powerful but has a steep learning curve and a heavy mental model. Hermes sits in the pragmatic middle: it borrows Bazel’s caching discipline, Task’s ergonomics, and a scripting model closer to a notebook than a build system.

The other differentiator is the profile system—portable bundles of tasks, environment definitions, and scripts that can be shared across teams. That’s where the community registry ClusterMob comes in: it’s effectively a package index for Hermes profiles, and it’s become the fastest way to bootstrap a new project’s automation.


🚀 Getting Started

Installation

Hermes ships as a single static binary, plus optional language runtimes for scripting tasks. The recommended install path is the official installer script, which verifies checksums and places the binary on your PATH.

# macOS / Linux (recommended)
curl -fsSL https://get.hermes.dev/install.sh | sh

# Verify
hermes --version
# hermes 3.4.1 (build 2026-10-14, linux/amd64)

# Homebrew
brew install hermes-dev/tap/hermes

# Windows (PowerShell)
irm https://get.hermes.dev/install.ps1 | iex

# Container image (useful for CI)
docker pull ghcr.io/hermes-dev/hermes:3.4

If you’re scripting in Python or JavaScript, install the corresponding runtime adapters:

hermes runtime add python   # uses your system python3 by default
hermes runtime add node
hermes runtime list

Hermes will detect existing python3/node binaries and record their versions in its runtime registry. You can pin versions per-project later.

Configuration

Every Hermes project has a manifest at its root. Here’s a minimal but realistic hermes.yaml:

# hermes.yaml
version: "3"

project:
  name: "acme-api"
  default_env: "dev"

env:
  dev:
    vars:
      API_URL: "http://localhost:8080"
      LOG_LEVEL: "debug"
  ci:
    vars:
      API_URL: "https://api.acme.test"
      LOG_LEVEL: "info"
    secrets_from: "env"   # read secrets from environment variables

runtimes:
  python: "3.12"
  node: "22"

tasks:
  install:
    desc: "Install dependencies"
    run: "pip install -r requirements.txt && npm ci"

  lint:
    desc: "Run linters"
    deps: [install]
    run:
      - "ruff check ."
      - "eslint src/"

  test:
    desc: "Run test suite"
    deps: [install]
    run: "pytest -q"
    cache:
      key: ["requirements.txt", "src/**/*.py", "tests/**/*.py"]

  build:
    desc: "Build production bundle"
    deps: [lint, test]
    run: "npm run build"
    outputs: ["dist/**"]

Global configuration lives at ~/.config/hermes/config.toml:

[telemetry]
enabled = false

[cache]
dir = "~/.cache/hermes"
max_size = "20GB"

[logging]
format = "jsonl"   # or "pretty"
level = "info"

[registry]
default = "https://registry.clustermob.dev"

Run hermes doctor after setup—it validates runtimes, checks cache permissions, and flags manifest issues before they bite you mid-task.


💡 Core Features

Feature 1: Declarative Task Graphs with Caching

Hermes tasks form a DAG via the deps field. The engine resolves the graph, runs independent branches in parallel, and caches outputs keyed on declared inputs.

tasks:
  fetch-schema:
    run: "curl -fsSL $API_URL/openapi.json -o schema.json"
    outputs: ["schema.json"]

  generate-client:
    deps: [fetch-schema]
    run: "openapi-generator generate -i schema.json -g typescript-fetch -o gen/"
    inputs: ["schema.json"]
    outputs: ["gen/**"]
    cache:
      key: ["schema.json", "openapi-generator.lock"]

  typecheck:
    deps: [generate-client]
    run: "tsc --noEmit"

The first hermes run typecheck fetches the schema, generates the client, and typechecks. The second run—if schema.json and the generator lockfile are unchanged—skips both upstream tasks and goes straight to tsc. In a real project I benchmarked, this cut a 47-second pipeline to 6 seconds on the warm path.

Real-world application: A frontend team that regenerates API clients on every pull request. With Hermes caching, the generator only runs when the OpenAPI spec actually changes, and CI logs show a clean cache hit line instead of a wall of generator output.

Feature 2: Embedded Scripting with Shared Context

When YAML isn’t enough, drop into a script. Hermes injects a ctx object with task outputs, environment variables, and a structured logger.

tasks:
  reconcile:
    deps: [fetch-schema]
    script: |
      // ctx is injected; this is JavaScript
      const schema = JSON.parse(ctx.readFile("schema.json"));
      const endpoints = Object.keys(schema.paths);
      ctx.log.info(`Found ${endpoints.length} endpoints`);

      const missing = endpoints.filter(e => !e.startsWith("/v"));
      if (missing.length) {
        ctx.log.warn("Non-versioned endpoints:", missing);
      }
      ctx.writeFile("endpoints.txt", endpoints.join("\n"));
    outputs: ["endpoints.txt"]

Python works the same way:

tasks:
  audit:
    script: |
      import json, sys
      data = json.loads(ctx.read_file("schema.json"))
      deprecated = [
          path for path, ops in data["paths"].items()
          if any(op.get("deprecated") for op in ops.values())
      ]
      ctx.log.info(f"{len(deprecated)} deprecated routes")
      if deprecated and ctx.env == "ci":
          sys.exit(1)   # fail CI if deprecations exist

Real-world application: A platform team uses a Python script task to enforce API governance—failing the build if deprecated endpoints are added without a removal date in the spec. It replaced 200 lines of bash and a fragile grep.

Feature 3: Profiles and the ClusterMob Registry

A profile is a named bundle of tasks, runtimes, and environment definitions that can be installed into any project. ClusterMob hosts community profiles.

# Search the registry
hermes profile search python

# Inspect before installing
hermes profile show clustermob/python-service

# Install into current project
hermes profile add clustermob/python-service

# Pin a version for reproducibility
hermes profile add clustermob/python-service@1.4.2

Installing a profile merges its tasks into your manifest under a namespace:

profiles:
  - name: "clustermob/python-service"
    version: "1.4.2"

You then get tasks like hermes run python-service:test, python-service:lint, and python-service:release. Profiles can also declare hooks—for example, a postinstall that writes a .pre-commit-config.yaml.

Real-world application: Onboarding. A new hire clones a repo, runs hermes profile add clustermob/python-service, and gets the same lint, test, and release tasks the rest of the team uses—no tribal knowledge required. This is the single biggest reason teams adopt Hermes over hand-rolled Makefiles.


🛠️ Advanced Workflows

Workflow 1: Multi-Stage Release with Rollback

This workflow builds, signs, publishes, and tags a release—with an automatic rollback if the smoke test fails.

tasks:
  release:
    desc: "Full release pipeline"
    deps: [build]
    script: |
      const version = ctx.env.VERSION;
      if (!version) throw new Error("VERSION required");

      ctx.log.info(`Releasing ${version}`);
      await ctx.exec("npm version", [version, "--no-git-tag-version"]);
      await ctx.exec("npm", ["publish", "--access", "public"]);

      // Smoke test against the published artifact
      const smoke = await ctx.exec("npx", [`acme-cli@${version}`, "--version"], { check: false });
      if (smoke.exitCode !== 0) {
        ctx.log.error("Smoke test failed, rolling back");
        await ctx.exec("npm", ["unpublish", `acme-cli@${version}`, "--force"]);
        throw new Error("Release rolled back");
      }

      await ctx.exec("git", ["tag", `v${version}`]);
      await ctx.exec("git", ["push", "origin", `v${version}`]);

Run it:

VERSION=2.3.0 hermes run release --env ci

The key detail: ctx.exec with check: false lets you inspect exit codes and decide policy in-script, rather than having the runtime abort immediately. That’s what makes rollback logic possible without shell gymnastics.

Workflow 2: Parallel Matrix Testing Across Runtimes

Hermes supports matrix expansion, so you can test across Python and Node versions in parallel without duplicating task definitions.

tasks:
  test-matrix:
    matrix:
      python: ["3.11", "3.12", "3.13"]
      node: ["20", "22"]
    runtime:
      python: "${{ matrix.python }}"
      node: "${{ matrix.node }}"
    run:
      - "pytest -q"
      - "npm test"
    cache:
      key: ["requirements.txt", "package-lock.json", "${{ matrix.python }}", "${{ matrix.node }}"]
# Runs all 6 combinations in parallel (respecting --jobs)
hermes run test-matrix --jobs 6

# Filter to a single combination
hermes run test-matrix --filter 'matrix.python==3.13'

Each combination gets its own cache namespace, so a change to package-lock.json invalidates only the Node-dependent entries—not the Python ones. On a 6-combination matrix, this typically saves 40–60% of warm-run time versus a naive setup.

For CI, export the graph and let your runner fan out:

hermes plan test-matrix --format json > plan.json
# Feed plan.json into your CI matrix generator

📊 Comparison with Alternatives

FeatureHermesMakeJustTask (go-task)Bazel
Declarative task DAG✅⚠️ (file-based)⚠️ (limited)✅✅
Content-addressed caching✅❌❌⚠️ (basic)✅
Embedded scripting (Python/JS)✅❌❌❌⚠️ (Starlark only)
Parallel execution with --jobs✅✅✅✅✅
Matrix expansion✅❌❌❌✅
Profile/registry ecosystem✅ (ClusterMob)❌❌❌⚠️ (rules)
Structured JSON logging✅❌❌⚠️✅
Learning curveLow–MediumMediumLowLowHigh
Cross-language orchestration✅⚠️⚠️⚠️✅

The honest caveats: Make is still faster to start for trivial projects, Bazel still wins on massive monorepos with hermetic builds, and Just has a more elegant syntax for pure shell-task workflows. Hermes wins when you need caching, scripting, and a shared profile ecosystem in one tool.


🎯 Pro Tips

  1. Cache aggressively, but key it correctly. The most common caching mistake is under-specifying the cache.key. Include lockfiles, runtime versions, and any env vars that affect output. A stale cache is worse than no cache—hermes run <task> --no-cache is your escape hatch when debugging.

  2. Use hermes plan before you run. hermes plan <task> --format json prints the resolved DAG, cache status, and estimated work. It’s invaluable in CI for catching accidental cache invalidations (e.g., a version bump that silently invalidates everything) before they cost you 20 minutes of runner time.

  3. Treat profiles as dependencies, and pin them. hermes profile add clustermob/python-service@1.4.2 is reproducible; hermes profile add clustermob/python-service is not. Pin in the manifest, update deliberately, and review profile diffs like you would any dependency bump. The ClusterMob registry shows changelogs and install counts—use them to judge whether a profile is maintained.

A bonus tip for teams migrating from Make: run hermes migrate make in a repo with an existing Makefile. It generates a starter hermes.yaml with your targets translated, which is a much faster starting point than writing from scratch.


🔗 Resources


Hermes won’t replace your build system if you’re already deep in Bazel, and it won’t replace Make for a 10-line project. But for the messy middle—multi-language repos, release choreography, and teams that need shared automation without shared tribal knowledge—it’s one of the most practical tools to pick up in 2026. Start with hermes init, translate one task, and see how quickly the rest follows.


Have questions? Join our Discord community or follow us on X.