CLAUDE.md · git:20261029.3b75326 · 2026-10-29 · sha256 77c9f0e479bc867f

CLAUDE.md git:20261029.3b75326A

Immutable. This exact content never changes and is served at /api/v1/blob/77c9f0e479bc867f.

# ihme-cli

iCloud Hide My Email CLI. Go + Cobra.

## Build

```
make              # build
make check        # vet + test + build
make cross        # cross-compile all platforms
```

## Architecture

```
cmd/
  ihme/main.go          Entry point (version from ldflags)
  root/root.go           Root command, subcommand registration
  auth/                  login (SRP + 2FA), status, logout
  list/                  List with --search/--active/--inactive/--tag/--sort
  new/                   Generate candidates, interactive pick, --address to reserve
  view/                  View single address by ref
  edit/                  Edit label/note/tags
  copy/                  Copy address to clipboard
  memorycmd/             Inspect the agent memory graph (path, search, graph, card)
  lifecycle/             deactivate, reactivate, delete (with --yes confirmation)
  export/                CSV/JSON export with filters
  forward/               forward-to management
  share/                 share <ref> | list | revoke: per-address access keys
  serve/                 ihme serve: the read-only web inbox (IMAP, app password
                         from --account/IHME_IMAP_*/em's accounts.json)

api/
  types.go               HmeEmail, SessionData, SavedCookie, auth types
  endpoints.go           URL constants, header maps
  client.go              HTTP client, manual Cookie header (rclone pattern), mergeCookies
  auth.go                SRP auth: start -> federate -> init -> complete -> 2FA -> trust -> accountLogin
  session.go             Session persistence to ~/.config/ihme/session.json
  hme.go                 HME CRUD: list, generate, reserve, update, deactivate, reactivate, delete

internal/
  srp/                   SRP-6a (NG_2048, SHA-256, NoUserNameInX)
  securitykey/           FIDO2 assertion over USB HID (cgo-free) for
                         security-key 2FA; touch only, no PIN
  cmdutil/               GetClient, OutputResult, ExactRefArg, RefOrPick +
                         PickAddress (omitted <ref> at a TTY -> newest-first
                         picker; never prompts without a TTY or with --json)
  shares/                shares.json: address -> SHA-256(key); Grant rotates,
                         Revoke removes; the server re-reads it per request
  inbox/                 ihme serve: IMAP source (exact To/Cc check before any
                         row or body is shown), MIME -> text, HTTP + embedded
                         templates; the privacy model is its package doc
  app/                   Application service: the six HME operations
                         shared by Cobra commands and agent tools
                         (both are adapters over it)
  agent/                 Embedded-agent adapter: BYOK config,
                         in-process tools, scoped-consent gate,
                         renderer (see `ihme new --agent`); also the
                         harness side (`--via`, via.go) and the MCP
                         server (mcpserve.go) for `ihme mcp`
  acp/                   Minimal Agent Client Protocol client (v1
                         subset, hand-rolled): ihme as the HARNESS
                         spawning claude-code/codex/opencode as the
                         model provider — see `ihme agent --via`
  memory/                Agent memory: a Logseq-style markdown graph
                         (journals/, pages/, flashcards). Plain files,
                         no DB — see its package doc

pkg/
  output/                Table, JSON, CSV, detail formatters
  filter/                --active/--inactive/--tag/--search/--sort
  tags/                  Parse/serialize #tag convention
  resolver/              Universal ref resolver (ID, email, label fuzzy)
  agentkit/              Embeddable agent kernel (stdlib-only, never
                         imports ihme packages — see its README)

skill/
  SKILL.md               Operational procedure shared by external
                         agents (shell) and the embedded agent
                         (go:embed, invoked as a task turn)

examples/
  toyagent/              Minimal agentkit consumer (live smoke)
```

## Embedded agent

Two entry points, one adapter (internal/agent):

- `ihme new <label> --agent [--grant ask|auto]` — scoped: runs the
  SKILL.md procedure; pre-grants ONE reservation for that label and
  touching only addresses created this run; everything else prompts.
- `ihme agent [task]` (alias `ihme --agent`) — general: interactive
  TUI without args, one-shot with a task; nothing is pre-granted.

A third mode inverts the provider: `ihme agent --via codex|claude|
opencode "<task>"` harnesses a full coding agent (its subscription
auth, its models — no BYOK key) as the brain. ihme is the ACP
client (internal/acp); the guest gets the HME operations back as
MCP tools by re-invoking this binary as `ihme mcp`. The consent
gate does NOT move to the guest: it runs inside the MCP child and
its cards travel over a unix socket to the harness terminal
(consentsock.go) — verified live that guest permission layers
(claude-agent-acp) execute mutating MCP tools without asking, so
ours is the only real gate. Tool physics (rationale floor, caps,
journaling) are unchanged — they live in tools.go, not the loop.

Code is the source of truth for the behavior; the load-bearing
comments live where the behavior lives:

- internal/agent/tui.go — interactive session: input authority,
  consent card, typed-reply redirect, reasoning status line
- internal/agent/gate.go — the scoped-consent policy
- internal/agent/interact.go — consent protocol (y/N/a/reply)
- internal/agent/config.go, auto.go — BYOK config keys and
  wire-protocol auto-detection
- internal/agent/run.go — system prompt, renderer, one-shot runs
- internal/agent/via.go — the --via harness: guest resolution,
  session/prompt loop, update rendering, consent socket wiring
- internal/agent/mcpserve.go — `ihme mcp`: the tool layer over MCP
  stdio, gate in front of every call
- internal/agent/consentsock.go — consent cards across the process
  boundary (why: the guest's permission layer never asks)
- internal/acp/client.go — the ACP v1 client subset and its types
- internal/agent/memtool.go — memory glue: the recall_memory /
  remember tools, the <memory> continuity block injected at session
  start, and the journal+page write made at reserve time
- internal/memory/memory.go — the graph store itself (journals,
  pages, flashcards, derived link graph); package doc has the design
- skill/SKILL.md — the operational procedure. RUNTIME PROMPT
  CONTENT (go:embed, invoked as a task turn): whenever tool schemas
  in tools.go change, its embedded-agent section must change with
  them, or the model reads two contracts.
- pkg/agentkit/README.md — kernel contract and invariants

Design rejections and their reasons: TASTE.md. When something needs
explaining, prefer (in order) a code comment at the behavior, a scar
in TASTE.md, or a where-to-look line here. Do not re-grow prose that
restates tui.go — it drifted within hours the last time; this
section has been composted once already.

## Auth flow

Apple SRP via idmsa.apple.com (web auth, not GSA):
1. GET  /authorize/signin — init session
2. POST /federate — submit email
3. POST /signin/init — SRP key exchange
4. POST /signin/complete — SRP proof (M1/M2)
5. 2FA: SMS (PUT /verify/phone) or device push (PUT /verify/trusteddevice/securitycode).
   Since ~mid-2026 Apple answers the securitycode POST with 409 even
   for a VALID code — the acceptance signal is the fresh
   X-Apple-Session-Token header, not the status (rclone#9488). Do
   not "restore" a strict 2xx check there.
   Security-key accounts get no code at all: GET /appleauth/auth
   carries fsaChallenge, internal/securitykey has a USB FIDO2 key
   sign it, POST /verify/security/key (pyicloud's request shape).
6. GET  /2sv/trust — trust token
7. POST setup.icloud.com/accountLogin — get webservices map + cookies

Cookie handling: manual Cookie header from session cookie list (rclone pattern).
No Go cookie jar domain matching — cookies go to all service requests.
Replayed byte-faithful: quoted values keep their quotes (SavedCookie.Quoted),
a name stored twice sends its last copy, a cookie Apple expires is dropped,
and a rotated cookie reaches disk through OnSessionUpdate on the call
that rotated it (inside the 15-min validate TTL nothing else saves).
CN accounts: auto-fallback to setup.icloud.com.cn on 421.

## Session resume

1. Load session + cookies from disk
2. If validatedAt is fresh (< 15 min), skip validation entirely —
   commands start straight on saved cookies
3. Otherwise validate (uses persisted cookies, no sign-in email).
   Failures are CLASSIFIED: only a definitive rejection (401/403/450
   or success=false) falls back to accountLogin; transport trouble
   (timeouts, 5xx, proxy flaps, 421 routing hiccups) gets one quiet
   retry, then surfaces as "iCloud temporarily unreachable — your
   session is probably still valid" WITHOUT touching accountLogin.
   421 is NOT expiry: Apple returns it for routing/rate pressure on
   valid sessions (the CN-region case is auto-retried separately).
4. accountLogin (triggers sign-in email) only on real rejection; its
   own transient failures also report as unreachable, not expired
5. Save updated session (cookies + validatedAt) after resume
6. A service host can still answer 401 inside that window — the
   mail-domain host holds its own cookies, so /validate passing is
   not its promise. api/hme.go recovers once: accountLogin (NOT
   validate, which already lied), rebuild the URL from the fresh
   webservices map and dsid, replay the call. A rejected call
   changed nothing on Apple's side, so replaying is safe even for
   mutations. Recovery is one shot, and retry loops against auth
   endpoints get accounts rate limited. A 401 on a session Apple
   minted in this process (accountLogin or the recovery itself) is
   NOT expiry: it is ServiceRefusedError, the account blocking web
   data access ("Allow Data Access", formerly "Access iCloud Data on
   the Web"; ADP turns it off). Field-confirmed (issue #14; user
   answer in docs/faq.md). It exits 1 and names the setting; never
   answer it with "sign in again". Fresh cookies go back to disk
   through Client.OnSessionUpdate.
7. clientId is generated once and persisted in the session — one
   installation, not a new stranger every invocation.

## Design rules

- Every command supports --json and --jq
- --json output includes hints with follow-up commands
- --help documents JSON response shapes
- <ref> resolves by anonymousId, email, or label (fuzzy)
- Errors include usage example and fix command; cmdutil.Explain is
  the single place that turns an error into user text (what
  happened / Cause / Fix) and cmdutil.ExitCode the single place
  that maps it to 0/1/2
- Error text never carries dsid or clientId (api.redactURL strips
  the query) — pasteable into an issue
- Errors to stderr, data to stdout
- Exit codes: 0 success, 1 error, 2 auth required
- Session at ~/.config/ihme/session.json (respects $XDG_CONFIG_HOME), 0600
- --verbose/-v is global, logs all HTTP requests

## Release

goreleaser on tag push. Archives include LICENSE, README, SKILL.md.