Inspired by Energy: extend `hermes import-agent` with Gemini CLI support

Energy's launch messaging leads with "import memories & skills — no fresh
start" as a core onboarding move. Hermes already imports Claude Code and
Codex setups; this widens the same no-fresh-start funnel to the third
major CLI agent, Google Gemini CLI (~/.gemini):

- GEMINI.md            -> memory entries in memories/MEMORY.md
- settings.json tools.allowed (run_shell_command(...)/ShellTool(...) rules,
  plus the legacy flat allowedTools key) -> config.yaml command_allowlist
- settings.json mcpServers -> config.yaml mcp_servers, honoring Gemini's
  httpUrl > url transport precedence; the per-server trust:true flag is
  deliberately dropped so Hermes approval settings stay untouched
- skills/<name>/SKILL.md -> skills/gemini-imports/<name>/
- extensions/ reported skipped with guidance (they bundle their own MCP
  servers/context)

Secrets rule unchanged: oauth_creds.json never read, secret-looking env
vars and Authorization headers stripped and reported.

Refactors the Claude allowlist merge into a shared _merge_command_allowlist
so both mappers use one merge path. 10 new tests (60 total in the file).
This commit is contained in:
Teknium 2026-08-07 19:17:08 -07:00
parent b3aa561faf
commit 3eb653c74d
No known key found for this signature in database
5 changed files with 323 additions and 28 deletions

View File

@ -1,9 +1,10 @@
"""hermes import-agent — import Claude Code / Codex CLI setups into Hermes.
"""hermes import-agent — import Claude Code / Codex / Gemini CLI setups into Hermes.
Usage:
hermes import-agent # auto-detect ~/.claude or ~/.codex
hermes import-agent # auto-detect ~/.claude, ~/.codex or ~/.gemini
hermes import-agent claude-code # import from ~/.claude
hermes import-agent codex # import from ~/.codex
hermes import-agent gemini # import from ~/.gemini
hermes import-agent claude-code --dry-run # preview only, no changes
hermes import-agent codex --source /path/to/.codex
@ -30,6 +31,20 @@ codex (~/.codex):
memories/*.md memory entries in HERMES_HOME/memories/MEMORY.md
skills/<name>/SKILL.md HERMES_HOME/skills/codex-imports/<name>/
gemini (~/.gemini, Google Gemini CLI):
GEMINI.md memory entries in HERMES_HOME/memories/MEMORY.md
settings.json tools.allowed config.yaml command_allowlist
(run_shell_command(...) / ShellTool(...) rules; legacy flat
``allowedTools`` is read too)
settings.json mcpServers config.yaml mcp_servers
(``httpUrl`` takes precedence over ``url``, matching Gemini's own
transport precedence; ``trust: true`` is deliberately dropped
Hermes approval settings stay untouched)
skills/<name>/SKILL.md HERMES_HOME/skills/gemini-imports/<name>/
extensions/ skipped with a note (extensions bundle
their own MCP servers/context; reinstall the Hermes-side equivalents
deliberately)
Secrets are NEVER imported: credential files (.credentials.json, auth.json)
are ignored, and MCP server env vars with secret-looking names (KEY, TOKEN,
SECRET, PASSWORD, ...) are stripped and reported so the user can re-add them
@ -59,16 +74,18 @@ ENTRY_DELIMITER = "\n§\n"
# default memory limit).
MEMORY_CHAR_LIMIT = 20_000
SUPPORTED_AGENTS = ("claude-code", "codex")
SUPPORTED_AGENTS = ("claude-code", "codex", "gemini")
_AGENT_DEFAULT_DIRS = {
"claude-code": ".claude",
"codex": ".codex",
"gemini": ".gemini",
}
_SKILL_CATEGORY = {
"claude-code": "claude-code-imports",
"codex": "codex-imports",
"gemini": "gemini-imports",
}
# Env var names that look like credentials — never copied into config.yaml.
@ -351,6 +368,37 @@ def claude_rule_to_command_pattern(rule: str) -> Optional[str]:
return inner
_GEMINI_SHELL_RULE_RE = re.compile(
r"^(?:run_shell_command|ShellTool)\((?P<inner>.*)\)$"
)
def gemini_rule_to_command_pattern(rule: str) -> Optional[str]:
"""Convert a Gemini CLI allowed-tool rule into a Hermes command glob.
Gemini's ``tools.allowed`` (and the legacy flat ``allowedTools``) lists
tool names that bypass the confirmation dialog. Shell rules look like
``run_shell_command(git status)`` or ``ShellTool(git status)`` and match
by command prefix, so they map to a trailing-glob Hermes pattern:
``run_shell_command(git status)`` ``git status*``
``ShellTool(npm test)`` ``npm test*``
``run_shell_command`` None (blanket rule, too broad)
Non-shell tool names (``write_file``, ``WebFetch``, ...) None: they
gate Gemini-specific tools with no command-allowlist equivalent.
"""
rule = (rule or "").strip()
m = _GEMINI_SHELL_RULE_RE.match(rule)
if not m:
return None
inner = m.group("inner").strip()
if not inner:
return None
if not inner.endswith("*"):
inner = inner + "*"
return inner
# ---------------------------------------------------------------------------
# Detection
# ---------------------------------------------------------------------------
@ -469,6 +517,8 @@ class AgentImporter:
return self.build_report()
if self.agent == "claude-code":
self._run_claude_code()
elif self.agent == "gemini":
self._run_gemini()
else:
self._run_codex()
return self.build_report()
@ -497,6 +547,24 @@ class AgentImporter:
self.import_memories_dir(self.source_root / "memories")
self.import_skills(self.source_root / "skills")
def _run_gemini(self) -> None:
# Inspired by the Energy scout finding "import memories & skills —
# no fresh start": widen import-agent to the third major CLI agent.
settings = self._load_gemini_settings()
self.import_context_file(self.source_root / "GEMINI.md", kind="gemini-md")
self.import_gemini_allowlist(settings)
mcp = settings.get("mcpServers")
self.import_mcp_servers(mcp if isinstance(mcp, dict) else {},
kind="mcp-servers")
self.import_skills(self.source_root / "skills")
extensions_dir = self.source_root / "extensions"
if extensions_dir.is_dir() and any(extensions_dir.iterdir()):
self.record(
"extensions", extensions_dir, None, "skipped",
"Gemini extensions bundle their own MCP servers and context — "
"re-add the servers you need to Hermes config.yaml deliberately",
)
# -- parsers (fail soft: bad files become per-item error records) -------
def _load_claude_settings(self) -> Dict[str, Any]:
@ -551,6 +619,24 @@ class AgentImporter:
return {}
return data if isinstance(data, dict) else {}
def _load_gemini_settings(self) -> Dict[str, Any]:
path = self.source_root / "settings.json"
if not path.exists():
self.record("settings", None, None, "skipped",
"No settings.json found")
return {}
try:
data = json.loads(read_text(path))
except (json.JSONDecodeError, OSError) as exc:
self.record("settings", path, None, "error",
f"Could not parse settings.json: {exc}")
return {}
if not isinstance(data, dict):
self.record("settings", path, None, "error",
"settings.json is not a JSON object")
return {}
return data
# -- mappers -------------------------------------------------------------
def import_context_file(self, source: Path, kind: str) -> None:
@ -635,11 +721,11 @@ class AgentImporter:
def import_permission_allowlist(self, settings: Dict[str, Any]) -> None:
"""settings.json permissions.allow → config.yaml command_allowlist."""
destination = self.target_root / "config.yaml"
permissions = settings.get("permissions")
allow = permissions.get("allow") if isinstance(permissions, dict) else None
if not isinstance(allow, list) or not allow:
self.record("command-allowlist", None, destination, "skipped",
self.record("command-allowlist", None,
self.target_root / "config.yaml", "skipped",
"No permissions.allow rules found")
return
@ -653,15 +739,63 @@ class AgentImporter:
patterns.append(pattern)
else:
skipped_rules.append(rule)
self._merge_command_allowlist(
source_label="settings.json permissions.allow",
patterns=patterns,
skipped_rules=skipped_rules,
empty_reason="No Bash(...) allow rules to import",
)
def import_gemini_allowlist(self, settings: Dict[str, Any]) -> None:
"""settings.json tools.allowed / allowedTools → config.yaml command_allowlist.
Gemini CLI stores auto-approved tools under the nested
``tools.allowed`` key (current schema) or the legacy flat
``allowedTools`` key; both are read.
"""
tools = settings.get("tools")
allowed = tools.get("allowed") if isinstance(tools, dict) else None
rules: List[Any] = list(allowed) if isinstance(allowed, list) else []
legacy = settings.get("allowedTools")
if isinstance(legacy, list):
rules.extend(legacy)
if not rules:
self.record("command-allowlist", None,
self.target_root / "config.yaml", "skipped",
"No tools.allowed rules found")
return
patterns: List[str] = []
skipped_rules: List[str] = []
for rule in rules:
if not isinstance(rule, str):
continue
pattern = gemini_rule_to_command_pattern(rule)
if pattern:
patterns.append(pattern)
else:
skipped_rules.append(rule)
self._merge_command_allowlist(
source_label="settings.json tools.allowed",
patterns=patterns,
skipped_rules=skipped_rules,
empty_reason="No run_shell_command(...) allow rules to import",
)
def _merge_command_allowlist(self, source_label: str,
patterns: List[str],
skipped_rules: List[str],
empty_reason: str) -> None:
"""Merge command patterns into config.yaml command_allowlist."""
destination = self.target_root / "config.yaml"
patterns = sorted(dict.fromkeys(patterns))
if not patterns:
self.record("command-allowlist", None, destination, "skipped",
"No Bash(...) allow rules to import",
unmapped_rules=skipped_rules)
empty_reason, unmapped_rules=skipped_rules)
return
config = self.load_target_config(
"command-allowlist", "settings.json permissions.allow", destination)
"command-allowlist", source_label, destination)
if config is None:
return
current = config.get("command_allowlist", [])
@ -670,7 +804,7 @@ class AgentImporter:
merged = sorted(dict.fromkeys(list(current) + patterns))
added = [p for p in merged if p not in current]
if not added:
self.record("command-allowlist", "settings.json permissions.allow",
self.record("command-allowlist", source_label,
destination, "skipped", "All patterns already present")
return
details: Dict[str, Any] = {"added_patterns": added}
@ -679,10 +813,10 @@ class AgentImporter:
if self.execute:
config["command_allowlist"] = merged
dump_yaml_file(destination, config)
self.record("command-allowlist", "settings.json permissions.allow",
self.record("command-allowlist", source_label,
destination, "imported", **details)
else:
self.record("command-allowlist", "settings.json permissions.allow",
self.record("command-allowlist", source_label,
destination, "imported", "Would merge patterns", **details)
def import_permission_denylist(self, settings: Dict[str, Any]) -> None:
@ -774,8 +908,12 @@ class AgentImporter:
)
if srv.get("cwd"):
hermes_srv["cwd"] = srv["cwd"]
if srv.get("url"):
hermes_srv["url"] = srv["url"]
# Gemini CLI uses ``httpUrl`` for streamable-HTTP servers and
# gives it precedence over ``url`` (SSE); Hermes takes either
# transport through its single ``url`` key.
remote_url = srv.get("httpUrl") or srv.get("url")
if remote_url:
hermes_srv["url"] = remote_url
headers = srv.get("headers")
if isinstance(headers, dict):
kept_headers = {
@ -866,13 +1004,14 @@ def import_agent_command(args) -> None:
detected = detect_agents()
if not detected:
print()
print_error("No supported agent setup found (~/.claude or ~/.codex).")
print_error("No supported agent setup found "
"(~/.claude, ~/.codex or ~/.gemini).")
print_info("Specify one explicitly: hermes import-agent claude-code --source /path")
return
if len(detected) > 1 and explicit_source is None:
print()
print_info("Multiple agent setups detected: " + ", ".join(detected))
print_info("Pick one: hermes import-agent claude-code or hermes import-agent codex")
print_info("Pick one: hermes import-agent claude-code | codex | gemini")
return
agent = detected[0]

View File

@ -14,10 +14,10 @@ def build_import_agent_parser(subparsers, *, cmd_import_agent: Callable) -> None
"""Attach the ``import-agent`` subcommand to ``subparsers``."""
parser = subparsers.add_parser(
"import-agent",
help="Import a Claude Code or Codex CLI setup into Hermes",
help="Import a Claude Code, Codex CLI, or Gemini CLI setup into Hermes",
description=(
"One-command import of another coding agent's setup into Hermes. "
"Maps CLAUDE.md/AGENTS.md instructions, permission allowlists, MCP "
"Maps CLAUDE.md/AGENTS.md/GEMINI.md instructions, permission allowlists, MCP "
"servers, skills, and memories into their Hermes equivalents. "
"Always shows a preview before making changes. API keys and "
"credentials are never imported — run 'hermes setup' for those."
@ -26,8 +26,8 @@ def build_import_agent_parser(subparsers, *, cmd_import_agent: Callable) -> None
parser.add_argument(
"agent",
nargs="?",
choices=["claude-code", "codex"],
help="Which agent to import from (default: auto-detect ~/.claude or ~/.codex)",
choices=["claude-code", "codex", "gemini"],
help="Which agent to import from (default: auto-detect ~/.claude, ~/.codex, or ~/.gemini)",
)
parser.add_argument(
"--source",

View File

@ -22,6 +22,7 @@ from hermes_cli.agent_import import (
claude_rule_to_command_pattern,
detect_agents,
extract_markdown_entries,
gemini_rule_to_command_pattern,
is_secret_key,
parse_existing_memory_entries,
sanitize_mcp_env,
@ -160,6 +161,63 @@ def codex_tree(profile_env):
return root
GEMINI_MD = """# Gemini instructions
- Use conventional commits
- Run tests before pushing
"""
@pytest.fixture()
def gemini_tree(profile_env):
"""Build a fake ~/.gemini tree (Google Gemini CLI)."""
root = profile_env / ".gemini"
root.mkdir()
(root / "GEMINI.md").write_text(GEMINI_MD, encoding="utf-8")
(root / "settings.json").write_text(json.dumps({
"tools": {
"allowed": [
"run_shell_command(git status)",
"ShellTool(npm test)",
"run_shell_command", # blanket → unmapped
"write_file", # non-shell → unmapped
],
},
"allowedTools": ["run_shell_command(make lint)"], # legacy flat key
"mcpServers": {
"docs": {
"command": "uvx",
"args": ["docs-mcp"],
"env": {
"DOCS_API_KEY": "secret-value",
"DOCS_REGION": "eu",
},
"trust": True, # must NOT leak into Hermes config
},
"streaming": {
"httpUrl": "https://mcp.example.com/mcp",
"url": "https://mcp.example.com/sse",
"headers": {
"Authorization": "Bearer abc123",
"X-Region": "us-east",
},
},
},
}), encoding="utf-8")
# OAuth credential file that must never be read/imported
(root / "oauth_creds.json").write_text(
json.dumps({"refresh_token": "SUPERSECRET"}), encoding="utf-8")
skill = root / "skills" / "gcp-deploy"
skill.mkdir(parents=True)
(skill / "SKILL.md").write_text(
"---\nname: gcp-deploy\n---\n\nDeploy to GCP.\n", encoding="utf-8")
# extensions dir → reported skipped
ext = root / "extensions" / "cloud-run"
ext.mkdir(parents=True)
(ext / "gemini-extension.json").write_text("{}", encoding="utf-8")
return root
def snapshot_tree(root: Path) -> dict:
"""Map of relative-path -> bytes for every file under root."""
return {
@ -186,6 +244,8 @@ class TestDetection:
def test_detects_claude_and_codex(self, claude_tree, codex_tree):
assert detect_agents() == ["claude-code", "codex"]
def test_detects_gemini(self, gemini_tree):
assert detect_agents() == ["gemini"]
def test_unsupported_agent_raises(self, hermes_home, tmp_path):
with pytest.raises(ValueError):
@ -201,6 +261,18 @@ class TestRuleMapping:
assert claude_rule_to_command_pattern("Read(~/.zshrc)") is None
assert claude_rule_to_command_pattern("WebFetch") is None
def test_gemini_shell_rules(self):
assert gemini_rule_to_command_pattern(
"run_shell_command(git status)") == "git status*"
assert gemini_rule_to_command_pattern(
"ShellTool(npm test)") == "npm test*"
def test_gemini_blanket_and_non_shell_rules_are_none(self):
assert gemini_rule_to_command_pattern("run_shell_command") is None
assert gemini_rule_to_command_pattern("ShellTool") is None
assert gemini_rule_to_command_pattern("write_file") is None
assert gemini_rule_to_command_pattern("WebFetch(example.com)") is None
class TestSecretDetection:
@pytest.mark.parametrize("key", [
@ -298,6 +370,77 @@ class TestCodexImport:
assert (hermes_home / "skills" / "codex-imports" / "db-migrate" / "SKILL.md").exists()
# ---------------------------------------------------------------------------
# Gemini CLI real run
# ---------------------------------------------------------------------------
class TestGeminiImport:
@pytest.fixture()
def report(self, gemini_tree, hermes_home):
return run_import("gemini", gemini_tree, hermes_home, execute=True)
def test_gemini_md_lands_in_memory(self, report, hermes_home):
memory = (hermes_home / "memories" / "MEMORY.md").read_text()
assert "conventional commits" in memory
assert "tests before pushing" in memory
def test_allowlist_lands_in_config_yaml(self, report, hermes_home):
config = yaml.safe_load((hermes_home / "config.yaml").read_text())
allow = config["command_allowlist"]
assert "git status*" in allow
assert "npm test*" in allow
assert "make lint*" in allow # legacy flat allowedTools
# blanket and non-shell rules must not leak in
assert "run_shell_command" not in allow
assert "write_file" not in allow
def test_unmapped_rules_reported(self, report):
item = next(i for i in report["items"]
if i["kind"] == "command-allowlist")
assert "run_shell_command" in item.get("unmapped_rules", [])
assert "write_file" in item.get("unmapped_rules", [])
def test_mcp_servers_land_in_config_yaml(self, report, hermes_home):
config = yaml.safe_load((hermes_home / "config.yaml").read_text())
docs = config["mcp_servers"]["docs"]
assert docs["command"] == "uvx"
assert docs["args"] == ["docs-mcp"]
assert docs["env"] == {"DOCS_REGION": "eu"}
# Gemini's trust flag must never reach Hermes config
assert "trust" not in docs
def test_http_url_takes_precedence_over_sse_url(self, report, hermes_home):
config = yaml.safe_load((hermes_home / "config.yaml").read_text())
streaming = config["mcp_servers"]["streaming"]
assert streaming["url"] == "https://mcp.example.com/mcp"
# Authorization header stripped, plain header kept
assert streaming["headers"] == {"X-Region": "us-east"}
def test_skill_copied(self, report, hermes_home):
assert (hermes_home / "skills" / "gemini-imports" / "gcp-deploy"
/ "SKILL.md").exists()
def test_extensions_reported_skipped(self, report):
items = {i["kind"]: i for i in report["items"]}
assert items["extensions"]["status"] == "skipped"
def test_no_secret_values_anywhere(self, gemini_tree, hermes_home):
run_import("gemini", gemini_tree, hermes_home, execute=True)
for p in hermes_home.rglob("*"):
if p.is_file():
content = p.read_text(errors="replace")
assert "secret-value" not in content
assert "SUPERSECRET" not in content
assert "Bearer abc123" not in content
def test_dry_run_writes_nothing(self, gemini_tree, hermes_home):
before = snapshot_tree(hermes_home)
report = run_import("gemini", gemini_tree, hermes_home, execute=False)
assert snapshot_tree(hermes_home) == before
assert report["dry_run"] is True
assert report["summary"]["imported"] > 0
# ---------------------------------------------------------------------------
# Secrets are never copied
# ---------------------------------------------------------------------------

View File

@ -89,7 +89,7 @@ hermes [global-options] <command> [subcommand/options]
| `hermes sessions` | Browse, export, prune, rename, and delete sessions. |
| `hermes insights` | Show token/cost/activity analytics. |
| `hermes claw` | OpenClaw migration helpers. |
| `hermes import-agent` | Import a Claude Code (`~/.claude`) or Codex CLI (`~/.codex`) setup. |
| `hermes import-agent` | Import a Claude Code (`~/.claude`), Codex CLI (`~/.codex`), or Gemini CLI (`~/.gemini`) setup. |
| `hermes dashboard` | Launch the web dashboard for managing config, API keys, and sessions. |
| `hermes serve` | Start the Hermes backend server (headless; powers the desktop app and remote backends). |
| `hermes desktop` (alias `gui`) | Build and launch the native Electron desktop app. |
@ -1533,15 +1533,15 @@ hermes claw migrate --source /home/user/old-openclaw
## `hermes import-agent`
```bash
hermes import-agent [claude-code|codex] [options]
hermes import-agent [claude-code|codex|gemini] [options]
```
Import a **Claude Code** (`~/.claude`) or **OpenAI Codex CLI** (`~/.codex`) setup into Hermes. Maps `CLAUDE.md`/`AGENTS.md` instructions to memory entries, `Bash(...)` permission allow/deny rules to `command_allowlist`/`approvals.deny`, MCP servers to `mcp_servers` in `config.yaml`, and skill directories into `~/.hermes/skills/`. Always previews before applying; API keys and credentials are never imported.
Import a **Claude Code** (`~/.claude`), **OpenAI Codex CLI** (`~/.codex`), or **Google Gemini CLI** (`~/.gemini`) setup into Hermes. Maps `CLAUDE.md`/`AGENTS.md`/`GEMINI.md` instructions to memory entries, permission allow rules (`Bash(...)` / `run_shell_command(...)`) to `command_allowlist`, MCP servers to `mcp_servers` in `config.yaml`, and skill directories into `~/.hermes/skills/`. Always previews before applying; API keys and credentials are never imported.
| Option | Description |
| --- | --- |
| `agent` | `claude-code` or `codex` (default: auto-detect). |
| `--source <path>` | Custom source directory (default: `~/.claude` or `~/.codex`). |
| `agent` | `claude-code`, `codex`, or `gemini` (default: auto-detect). |
| `--source <path>` | Custom source directory (default: `~/.claude`, `~/.codex`, or `~/.gemini`). |
| `--dry-run` | Preview only — write nothing. |
| `--overwrite` | Replace conflicting MCP servers / skills (default: skip). |
| `--yes`, `-y` | Skip confirmation prompts. |

View File

@ -1,17 +1,18 @@
---
sidebar_position: 9
title: "Import from Other Agents"
description: "One-command import of a Claude Code (~/.claude) or OpenAI Codex CLI (~/.codex) setup into Hermes — instructions, allowlists, MCP servers, skills, and memories."
description: "One-command import of a Claude Code (~/.claude), OpenAI Codex CLI (~/.codex), or Google Gemini CLI (~/.gemini) setup into Hermes — instructions, allowlists, MCP servers, skills, and memories."
---
# Import from Other Agents
`hermes import-agent` imports your existing **Claude Code** or **OpenAI Codex CLI** setup into Hermes with one command. It follows the same preview-first pattern as [`hermes claw migrate`](../guides/migrate-from-openclaw.md): you always see a per-item plan before anything is written, and `--dry-run` never touches disk.
`hermes import-agent` imports your existing **Claude Code**, **OpenAI Codex CLI**, or **Google Gemini CLI** setup into Hermes with one command. It follows the same preview-first pattern as [`hermes claw migrate`](../guides/migrate-from-openclaw.md): you always see a per-item plan before anything is written, and `--dry-run` never touches disk.
```bash
hermes import-agent # auto-detect ~/.claude or ~/.codex
hermes import-agent # auto-detect ~/.claude, ~/.codex, or ~/.gemini
hermes import-agent claude-code # import from ~/.claude
hermes import-agent codex # import from ~/.codex
hermes import-agent gemini # import from ~/.gemini
hermes import-agent claude-code --dry-run # preview only
hermes import-agent codex --source /path/to/.codex # custom location
hermes import-agent claude-code --overwrite --yes # replace conflicts, skip prompts
@ -41,9 +42,21 @@ Claude's `Bash(npm run test:*)` prefix rules become `npm run test*` globs. Non-`
| `memories/*.md` | Memory entries in `~/.hermes/memories/MEMORY.md` |
| `skills/<name>/` (dirs with `SKILL.md`) | `~/.hermes/skills/codex-imports/<name>/` |
### Gemini CLI (`~/.gemini`)
| Gemini CLI | Hermes |
|---|---|
| `GEMINI.md` (global instructions) | Memory entries in `~/.hermes/memories/MEMORY.md` |
| `settings.json``tools.allowed` (`run_shell_command(...)` / `ShellTool(...)` rules; legacy flat `allowedTools` too) | `command_allowlist` in `config.yaml` |
| `settings.json``mcpServers` | `mcp_servers` in `config.yaml` |
| `skills/<name>/` (dirs with `SKILL.md`) | `~/.hermes/skills/gemini-imports/<name>/` |
| `extensions/` | Skipped with a note — extensions bundle their own MCP servers/context; re-add what you need deliberately |
Gemini's shell rules match by command prefix, so `run_shell_command(git status)` becomes the `git status*` glob. Non-shell tool names (`write_file`, ...) are reported as unmapped. For MCP servers, `httpUrl` takes precedence over `url` (matching Gemini's own transport precedence), and the per-server `trust: true` flag is deliberately dropped — Hermes approval settings stay untouched.
## What is never imported
**API keys and credentials.** Credential files (`~/.claude/.credentials.json`, `~/.codex/auth.json`) are never read, and MCP server environment variables or headers with secret-looking names (`*_TOKEN`, `*_API_KEY`, `Authorization`, ...) are stripped and listed in the report so you can re-add them deliberately. Run `hermes setup` to configure providers, or add secrets to `~/.hermes/.env`.
**API keys and credentials.** Credential files (`~/.claude/.credentials.json`, `~/.codex/auth.json`, `~/.gemini/oauth_creds.json`) are never read, and MCP server environment variables or headers with secret-looking names (`*_TOKEN`, `*_API_KEY`, `Authorization`, ...) are stripped and listed in the report so you can re-add them deliberately. Run `hermes setup` to configure providers, or add secrets to `~/.hermes/.env`.
## Behavior notes