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.
Auth — Auth0 device login (recommended)
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:
- Trades the Auth0 access token for an Ermes admin JWT via
/api/cli/exchange(the account's email must be verified in Auth0). - Lists every project you can access via
/api/admin/projects/all. - Caches the list in
~/.ermes/projects.yml(chmod 600) — no keys are minted at login. - Auto-selects the first project as active.
- On the first command against a project, mints your own
sk_key for it viaPOST /api/admin/projects/:id/cli-key, namedcli-<email>-<hostname>and bound to your admin account (revoked when you are removed from the project). Keys from the oldcli-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/apiset on the Ermes API (SSM env-file). Without it,/api/cli/exchangereturns 404 andermes loginerrors 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.
--jsonis a ROOT flag, not ametricsflag —ermes --json metrics …, neverermes metrics --json(that exits withNo such option: --json). This catches people out because every example that shows JSON also shows a subcommand. The per-campaign rows land underresults(notcampaignsorrows), alongsidechannel,summary,truncatedandaggregated_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 allthe shape nests instead:{"channel": "all", "channels": {"email": {…}, "push": {…}}}, so a consumer must branch onchannelbefore readingresults.
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ermes_cli-1.1.0.tar.gz | 90.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.