Skip to main content

Ermes CLI

This package gives Ermes operators a CLI surface for explicit-project API reads and guarded writes.

Version 1.1.0. It removed the ermes-mcp compatibility server that 1.0.0 shipped (DEV-2326); a machine that registered it must drop that registration (e.g. claude mcp remove ermes).

Install the CLI

pipx install ermes-cli              # from PyPI; `pipx upgrade ermes-cli` later
ermes version

Or run the installer, which also checks Python/pipx and verifies the entrypoints:

./cli/scripts/install-cli.sh                  # latest release
./cli/scripts/install-cli.sh --version=1.0.0  # pinned
./cli/scripts/install-cli.sh --branch=main    # developers: straight from git

Releases are cut by pushing a tag ermes-cli-vX.Y.Z equal to cli/pyproject.toml's version (.github/workflows/publish-cli.yml runs the tests, builds the wheel, publishes to PyPI and attaches it to a GitHub Release). install-cli.sh is idempotent and safe to re-run for upgrades.

Update the CLI

Automatic prompt. In an interactive terminal, the CLI checks PyPI for a newer ermes-cli at most once every 24 hours (2-second timeout, cached in ~/.ermes/update-check.json; failed checks are cached too, so an offline laptop is not slowed down). When one exists it asks before running your command:

ermes-cli 1.2.0 is available (you have 1.1.0). Update now? [Y]es / [n]ot now / [s]kip this version
  • Yes (Enter): runs pipx upgrade ermes-cli, checks the new version in a fresh process, then re-runs your command on it.
  • Not now: runs your command; asks again on the next run.
  • Skip this version: never asks about that version again; a newer release asks once more.

A failed upgrade or Ctrl-C at the prompt prints one line and runs your command on the current version. The check never runs with --json, when CI is set, when stdin/stdout/stderr is not a TTY, before ermes version / ermes update, or with ERMES_NO_UPDATE_CHECK=1.

Manual.

ermes update            # check, then ask before upgrading
ermes update --check    # report only, never install
ermes update --yes      # upgrade without asking
ermes --json update [--yes]   # --json installs only with --yes

Status values (--json status): up_to_date, ahead, update_available, updated, lookup_failed (exit 1), update_failed (exit 1). Installer output goes to stderr; stdout stays one JSON document.

Supported install. Only the pipx install of the PyPI package (install-cli.sh, or pipx install ermes-cli) upgrades itself. pipx upgrade keeps the old venv if pip fails, so a failed upgrade never leaves you without a CLI. Editable checkouts (git pull), git or local-dir installs, non-pipx environments and an ermes that is not the venv pipx manages get the manual command instead of a prompt. Keychain tokens and ~/.ermes are untouched.

ermes login                             # Auth0 device flow + auto-discover projects
ermes projects list                     # show cached projects; keys stay masked
ermes use <project-name-or-id>           # interactive active-project selection
ermes whoami                            # local selection only; not API liveness
ermes projects refresh                  # re-sync after admins add you to new projects
ermes logout                            # delete Keychain tokens + projects.yml

ermes login runs an Auth0 Device Authorization Flow (RFC 8628) against the shared dev-hqiwr-re.us.auth0.com tenant (same one publinker-cli uses, so the authenticated user is your Auth0 identity). After authentication:

  1. Trades the Auth0 access token for an Ermes admin JWT via /api/cli/exchange (the account's email must be verified in Auth0).
  2. Lists every project you can access via /api/admin/projects/all.
  3. Caches the list in ~/.ermes/projects.yml (chmod 600) — no keys are minted at login.
  4. Auto-selects the first project as active.
  5. On the first command against a project, mints your own sk_ key for it via POST /api/admin/projects/:id/cli-key, named cli-<email>-<hostname> and bound to your admin account (revoked when you are removed from the project). Keys from the old cli-managed-<hostname> scheme are ignored.

Auth0 tokens live in the OS keychain (ermes-cli service). On a headless box with no keyring backend they fall back to ~/.ermes/auth0-tokens.json (chmod 600). Per-project sk_ keys live in ~/.ermes/projects.yml.

For interactive work, ermes use <project-name-or-id> persists the active project. For every script or agent command, pin scope without changing that shared default:

ERMES_PROJECT=<project-name-or-id> ermes --json campaigns list --limit 1

Resolution order: --profile, ERMES_API_KEY, ERMES_PROJECT, the active project, then the legacy ERMES_PROFILE / config.yml default. ERMES_PROFILE is deprecated and prints a warning; it can no longer override an explicit project or projects.yml.

For CI or a machine without a browser, skip login and pass a key directly:

ERMES_API_KEY=sk_... ERMES_BASE_URL=https://ermes.publinker.com ermes --json campaigns list

ermes whoami reports the local selection; ermes whoami --check additionally makes one authenticated read and exits 1 if the key is rejected.

Requires: AUTH_CLI_AUDIENCE=https://ermes.publinker.com/api set on the Ermes API (SSM env-file). Without it, /api/cli/exchange returns 404 and ermes login errors out with a clear hint.

Legacy: single-profile manual setup

ermes config init (or ermes config add) still works for the one-project, manual-paste flow. It writes ~/.ermes/config.yml with the legacy profile shape; resolve_profile() falls back to it when no projects.yml exists. The command now emits a deprecation warning pointing at ermes login.

Write-mode safety (default-blocked mutations)

Every POST / PATCH / PUT / DELETE through the CLI is blocked by default. Reads work freely; mutations require an explicit unlock. These interactive commands act on the project deliberately selected with ermes use:

ermes write status                    # show current state
ermes write allow                     # unlock for 30 minutes
ermes write allow --minutes 5         # custom window (1-1440 min)
ermes write allow --persistent        # no expiry (until `write deny`)
ermes write deny                      # close the window immediately

For one reviewed scripted mutation, keep both project and write authorization process-scoped:

ERMES_PROJECT=<owned-project-name-or-id> ERMES_ALLOW_WRITES=1 \
  ermes --json <resource-group> <mutation> <arguments>

Ermes has no general server-side dry run. Read the current resource before the mutation, keep the scope to an owned project, then read it again to verify the requested state. Never leave ERMES_ALLOW_WRITES exported in a shared shell.

State lives in ~/.ermes/state.yml (chmod 600), separate from projects.yml so refreshing projects doesn't clobber the write window. ermes whoami shows the current write status.

Every agent write must carry explicit project scope and one-process ERMES_ALLOW_WRITES=1 authorization.

Metrics

These examples assume that you deliberately selected an interactive project with ermes use. In scripts, prefix the same commands with ERMES_PROJECT=<project-name-or-id>.

ermes metrics                           # last 7 days, type=trigger, channel=email (UI default)
ermes metrics --type blast --days 30    # last 30 days, blast campaigns only
ermes metrics --type all --start 2026-05-01 --end 2026-05-31
ermes metrics --channel push --days 7   # push campaigns
ermes metrics --channel all --days 7    # both, as two labelled blocks
ermes --json metrics --type all         # raw JSON for piping

Per-campaign breakdown: sends, opens, clicks, bounces, complaints, plus computed open/click/CTOR rates. Aggregate summary row at the bottom. Server caps at 100 rows / 90-day range; the CLI surfaces both as warnings.

Channels

--channel / -c takes email (default) | push | all. The API accepts only email and push, lowercase — the CLI folds the case so --channel PUSH works instead of 400ing, and defaults to email, which is also what the server assumes when the param is omitted, so the default output is unchanged.

There is no combined mode server-side: --channel all issues one request per channel and prints two labelled blocks (--json nests them under channels). The two are never summed — push has no opens or bounces, so the rates aren't comparable.

--json is a ROOT flag, not a metrics flag — ermes --json metrics …, never ermes metrics --json (that exits with No such option: --json). This catches people out because every example that shows JSON also shows a subcommand. The per-campaign rows land under results (not campaigns or rows), alongside channel, summary, truncated and aggregated_through:

{"channel": "push", "results": [{"campaign_id": 156139, "name": "...",
 "sends": 61, "failed": 0, "clicks": 0, "click_rate": 0, "opens": 0}],
 "summary": {"campaign_count": 13, "sends": 499, "failed": 67, "clicks": 25},
 "truncated": false, "aggregated_through": "2026-08-04"}

For --channel all the shape nests instead: {"channel": "all", "channels": {"email": {…}, "push": {…}}}, so a consumer must branch on channel before reading results.

Push renders the UI's columns — Sent (= sends + failed), Delivered (= sends), Failed, Clicks, Click %. Opens / Open % / CTOR / Bounces / Spam are omitted: push has no open or bounce event, so they are always zero and only read as breakage.

aggregated_through in the response is the newest local day with data for that channel+type, capped at the day the hourly rollup has marked complete for the project. Today, and yesterday until the first run past local midnight, are returned as partial "so far" numbers, so the default window ends yesterday, like the UI; pass --end to include today. The empty-state message prints the frontier so an empty window is legible.

Tool surface

CLI (ermes ...)

Group Commands
campaigns list, get, create, update, delete, trigger (--external-id, optional --email/--phone/--device-token/--locale/--timezone)
templates list, get, create, update, delete
lists list, get, create, update, delete
journeys list, get, create, update, delete
users list, get, delete, events, lists
keys list, get, create, update, delete — uses your Auth0 admin session, never a project key
subscriptions, tags, providers, ingest, export, config …
login, logout, whoami, use, projects Auth0 device-flow auth + multi-project context
metrics Campaign metrics, email or push (mirrors UI Metrics view)
write allow / deny / status Control the write-mode guard (mutations blocked by default)

Full surface: ermes --help.

Where in the wider stack

Ermes is a CLI-first integration in the Martech-DM/martech-mcps registry. That repo's ermes/README.md and ermes/skills/ermes-ops/SKILL.md are the cross-fleet setup and operating runbooks.

CLI source history

The ermes_cli/ package was originally bundled inside the legacy ermes-skill. It now lives here as the single code source of truth. The versioned operating skill lives at martech-mcps/ermes/skills/ermes-ops and invokes this installed package rather than carrying a copy.

Metadata

Release files for ermes-cli 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ermes-cli 1.1.0
File Size Uploaded Provenance
ermes_cli-1.1.0.tar.gz 90.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ermes-cli 1.1.0
File Interpreter ABI Platform
ermes_cli-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 158.6 kB

Release files / ermes_cli-1.1.0.tar.gz

Download URL ermes_cli-1.1.0.tar.gz
Size 90.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1f85b4986e2f57af9d453b6f8fb6d7ed82fc19d0973b43e3a9760c55a0733abf
BLAKE2b-256 checksum
How to use checksums
ec22333ca78d5dcf95fa2cf38368f6b3a153d042c8cc996db03d2022489b8c03
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / ermes_cli-1.1.0-py3-none-any.whl

Download URL ermes_cli-1.1.0-py3-none-any.whl
Size 67.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a544fe37379d92c1c03ac7b3c982cdb867a97e213a6178fc2dc58fc9d5ce7197
BLAKE2b-256 checksum
How to use checksums
08a5b7ac204e38812d1f870f4a9454bd63ab575b8638b09556c63ba4cfed5ba2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for ermes-cli 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ermes_cli-1.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page