Your Profile Export Was Shipping Your Cloud Credentials


You spend a morning tuning a profile, then hand it to a teammate: hermes profile export work -o work.tar.gz, with a note saying “unpack this and you have my setup”. They unpack it and they do have your setup — plus your private registry credentials. The tar.gz contains skills/, config.yaml, state.db, and a .docker/config.json you never thought about.

That was not a hypothetical hole. hermes profile export excluded some credential files but missed the OS-level credential directories entirely. Two PRs merged on October 3 and October 4 (#132201 and #132654) closed it. Here is what leaked, why, and what you should check today.

Stage one: .ssh, .aws, .gnupg, .kube

An earlier fix (#130525) already kept Hermes’ own secrets — auth.json, .env, OAuth tokens — out of exports. But PROFILE_CREDENTIAL_PATHS only covered the subprocess home/ subtree; the operating-system stores were simply not on the list.

#132201 added the first batch: .ssh, .aws, .gnupg, .kube, and direnv’s .envrc. It also fixed two neighbouring problems:

  • Nesting. A skill directory copied from a real home can carry skills/some-skill/.ssh/id_rsa. The old export filter matched top-level names only, so a nested .ssh shipped anyway. After the fix those stores are dropped at any depth.
  • Distribution installs. When you install a profile distribution, those paths are marked user-owned (profile_path_is_private), so a package can no longer plant or overwrite them on your machine.

Stage two: .docker, .azure, .config/gh, .config/gcloud

A day later, #132654 noticed four sibling directories were still uncovered — and these were exactly the directories agent/file_safety.py already write-denies. Hermes refused to write them, then happily exported them:

Directory What usually lives there
.docker/config.json Registry auth for private images
.azure/accessTokens.json Azure CLI access tokens
.config/gh/hosts.yml Your GitHub CLI token
.config/gcloud/ Google Cloud ADC and credentials.db

The @file context reference made it worse: writing @file:~/.config/gcloud/application_default_credentials.json in a message expanded the token into context. And the gateway feeds untrusted remote text through @file expansion, so it was the same leak class.

Why it happened: three hand-copied lists

The root cause was not one forgotten directory. The same list had been copied by hand in three places, and the copies had drifted:

  1. the write denylist in agent/file_safety.py;
  2. the export filter in hermes_cli/profiles.py;
  3. the @file refusal list in agent/context_references.py.

The fix gives the list a single source. agent/file_safety.py now holds:

HOME_CREDENTIAL_DIRS = (".ssh", ".aws", ".gnupg", ".kube",
                        ".docker", ".azure", ".config/gh", ".config/gcloud")

build_write_denied_prefixes derives from it, and hermes_cli/profiles.py unpacks the same tuple into _OS_CREDENTIAL_STORES, so the export lists (PROFILE_CREDENTIAL_PATHS, _EXPORT_CREDENTIAL_FILES) can no longer drift away from the write denylist. agent/context_references.py imports it too, which is how @file gained .config/gcloud.

The export filter also changed how it matches — on trailing path components rather than top-level names:

def _export_credential_entries(directory: str, contents: list) -> set:
    """Entries of *directory* that are an _EXPORT_CREDENTIAL_FILES store: matched on trailing path
    components, so ``.config/gh`` drops at any depth while the rest of ``.config`` ships."""

So .config/gh and .config/gcloud are dropped wherever they sit, while unrelated content such as .config/other, .config/ghost or .config/gh.bak still exports normally.

Already exported one? Assume it is exposed

This fix only protects exports made after it. If you have shared a profile archive — with a teammate, a file host, or a git repo — the right response is rotation, not deleting the file:

  1. Look at what is actually inside the archive first (a tar.gz lists without unpacking):
tar -tzf work.tar.gz | grep -E '(^|/)\.(ssh|aws|gnupg|kube|docker|azure|envrc)/|\.config/(gh|gcloud)/'
  1. Anything that matches is burned: replace SSH keys, rotate AWS / Azure / gcloud credentials, revoke and reissue the GitHub token, change the private registry password.
  2. Export again, confirm it is clean (the same grep returns nothing), then share that.

The short version

  • hermes profile export produces a tar.gz and now excludes Hermes’ own credential files plus eight home-level credential directories;
  • the gap existed because three hand-copied lists drifted apart; the fix makes one list authoritative and matches on trailing path components;
  • @file expansion and distribution installs share that list, so all three tightened together;
  • the fix is forward-looking — rotate anything you already shipped.

For the wider isolation picture, see the three outbound lanes for secret redaction and renaming a profile without losing its identity; the full subcommand surface is in the hermes profile reference.