mcp-google-multi
CLAUDE.md21 starsChanged 30 days agoMITView on GitHub
Conventions for AI assistants modifying this codebase. v6 is user-OAuth only: stdio by default, optional Streamable HTTP (MCP_TRANSPORT=http|both) behind a built-in OAuth 2.1 authorization server; one owner identity context per transport, with seams (hint provider, boot gates, tenant purge) a multi-tenant host can fill. User-facing rationale for the big mechanisms (discover-first, fan-out, trimming, write-control, escape hatch) lives in docs/features.md; implementation rationale too long for a code comment (Windows rename retry, GET/HEAD body stripping, admin-scope gating) lives in docs/internals.md. Keep both…
- Reads credentials
In this repositoryOne other file it gives its agents.
- google-workspaceSkill
The file
We split this CLAUDE.md into the rules it gives an agent
10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 83
Add a ServiceEntry to SERVICES (src/services.ts), with an enabled() gate for optional bundles — buildRegistry() picks it up.
- Very specific: line 84
Run npm run gen:coverage to regenerate COVERAGE.md (never hand-edit it), and update the tool/service counts in README.md, docs/features.md …
- Very specific: line 89
When a new curated tool covers a Discovery method, append the methodId to CURATED_METHOD_IDS and regenerate so the generated duplicate …
- Very specific: line 98
Always go through the injected getClientFn(account) (handles refresh + re-encrypt).
- Very specific: line 113
Every fileId call: supportsAllDrives: true.
- Very specific: line 126
After a release, .github/workflows/backmerge.yml resyncs the source branch forward: stable = main → staging + dev, beta = staging → dev.
- Very specific: line 136
Don't hardcode aliases — read them from the registry (server.accountAliases() / server.accountSet()).
- Very specific: line 137
Don't bypass getClient; don't write plaintext tokens or perms wider than 0o600.
- Distinctive: line 138
Don't re-implement error mapping or write gating — use the shared helpers.
- Distinctive: line 139
Don't add narrative comments — non-obvious WHY only.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Working on mcp-google-multi
Conventions for AI assistants modifying this codebase. v6 is user-OAuth only: stdio by default, optional Streamable HTTP (MCP_TRANSPORT=http|both) behind a built-in OAuth 2.1 authorization server; one owner identity context per transport, with seams (hint provider, boot gates, tenant purge) a multi-tenant host can fill. User-facing rationale for the big mechanisms (discover-first, fan-out, trimming, write-control, escape hatch) lives in docs/features.md; implementation rationale too long for a code comment (Windows rename retry, GET/HEAD body stripping, admin-scope gating) lives in docs/internals.md. Keep both in sync when behavior changes — comments stay one-line and point there.
Project shape
src/index.ts— entry.main()dispatches the CLI commands (auth,migrate-tokens,migrate-config,doctor,reset,metrics,write-client-config,account export|import,config check), else resolves the transport (src/http-config.ts), the master key and usage metrics, then builds oneMcpServer+ identity context + registry per transport: stdio, and/or HTTP (forcescuratedmode, mounts the OAuth AS, refuses to boot withoutMCP_OWNER_EMAILSunless multi-tenant boot gates are installed). Each registry'sinstallListHandler()runs before connect.src/compose.ts—buildRegistry(server, ctx, mode?, metrics?)registers every enabled service (the curatedSERVICEStable plusGENERATED_SERVICES, gates insrc/services.ts, all filtered byGOOGLE_TOOLSETS), fails fast ifregistry.services()is empty (BEFORE meta tools register — escape tools would mask an empty service set), thenregisterDiscoverTools()+registerEscapeTools()+registerAccountTools()+registerDiagnoseTool(), plusregisterAccountWizardTools()for the owner context only.requestHooksFor()builds the per-request arg-normalization / unknown-arg / validation-envelope hooks both transports share. No import-time side effects.src/registry.ts—ToolRegistrywraps the MCP server: records aToolEntryper tool (name, service, cud, description, inputShape, annotations, clientMeta?, meta, generated, irreversible, requiredScopes?), derivescud(read/create/update/delete) from the tool name viainferCud, enforces write-control by wrapping every CUD handler, injects computed annotations (readOnlyHint/destructiveHint;anthropic/requiresUserInteractiononIRREVERSIBLE_TOOLS), and owns discover-first visibility: all tools register eagerly (always callable — graceful dispatch), but the customtools/listhandler advertises perGOOGLE_DISCOVERYmode (lazydefault = meta +reveal()ed services;curated= + all curated;eager= everything;generatedprovenance on ToolEntry from the explicit-cud marker; tools no catalog bundle can authorize are never advertised).discover_all/discover_resetmetas toggle a runtime lazy→curated overlay (expand()/collapse(), both firetools/list_changed; collapse also clears reveals — a client mishandling a shrinking list degrades collapse to a session no-op, zero correctness impact).hideServices(services)keeps whole services out oftools/listwhatever the mode or a later reveal (hidden, not unregistered: still callable by name and listed by{service}_discover; meta tools never hidden; each call replaces the earlier set) for a host that serves one registry per identity. ThehttpConfirmfield (HttpConfirm, seesrc/http-confirm.ts) decides how an escape-hatch write is confirmed over HTTP.installListHandler()must run after registration, before connect. Service files callserver.registerTool(...)—serveris aToolRegistry.src/discover.ts—registerDiscoverTools(): one eager{service}_discovermeta-tool per service; returns the catalog (registry.catalog), reveals the service, triggerstools/list_changed. Never emittool_referencecontent blocks or top-leveldefer_loadingfields — strict SDK clients reject/strip them (a client-side constraint, independent of the server SDK version).src/toolsets.ts—GOOGLE_TOOLSETSparsing (all| CSV of service names).src/discovery-client.ts— runtime Google API Discovery:loadMethodIndex(api)fetcheshttps://www.googleapis.com/discovery/v1/apis/{id}/{version}/reston demand, disk-caches 7 days (DISCOVERY_CACHE_PATH), stale-if-offline;cudFromMethod(HTTP verb → cud, POST read-verbs like batchGet → read),expandPath({param} + {+param} templates),searchMethods. Deps injectable for tests — never hit the network in unit tests.src/tools/google-api.ts—registerEscapeTools(): eagergoogle_api_search+google_api_call. The call tool enforces write-control ITSELF (its registry cud is read, so the wrapper doesn't gate it) viaisAllowedon the method's derived cud — keep that check when touching this file.src/fanout.ts— cross-account fan-out:parseAccountSelector("*"| CSV | single),runFanout(bounded 5, per-account isolation, merged{results, partial}envelope with parsed payloads; never embed raw JSON strings),fanoutAccountField(union schema: a live*-or-alias check | CSV regex; the set is read at parse time). Wired inregistry.ts: read tools only, never meta (google_api_callinfers cud=read but executes writes), neverFANOUT_EXCLUDE(read tools that write local files). Fan-out wraps the raw handler INNERMOST so compaction runs once on the merged envelope.src/tools/accounts-tool.ts— eageraccount_listmeta tool +deriveAccountHealth(pure, deps-injectable). Reads the context's token store throughaccountHealthDepsFor(ctx.tokenStore)— NEVERgetClient(would attach refresh listeners).readTokenthrows on decrypt failure (only returns null when missing) — keep the per-account try/catch. Never output token values.src/trim.ts— response trimming:compactResult(registry re-serializes every JSON text output compactly;GOOGLE_TRIM=offopts out — compaction only, never the caps),capText(paging caps with truncated/totalChars),sliceClean(permanent caps — drops a trailing lone surrogate). Fat readers own their caps:drive_readmaxChars/offset, gmail reads 50k body cap +fullparam,calendar_list_eventsANDcalendar_list_instanceslist-modeformatEvent(e, {full:false}). New fat-payload tools should reuse these helpers.src/write-control.ts—resolvePolicy()(env → policy) +isAllowed()(deny-by-default verdict) +denyReason()/writeDisabledResult()+describePolicy()(config checkrendering) +IRREVERSIBLE_TOOLS.src/outbound-allowlist.ts— opt-inGOOGLE_OUTBOUND_ALLOWLISTrecipient gate (Gmail recipients, Calendar attendees, Drive grantees, escape-hatch bodies); off when unset.src/token-store.ts— encrypted token store (AES-256-GCM, key fromresolveMasterKey());readToken/writeToken/updateToken/hasTokenover the owner dir,createTokenStore(dir, {tenantId?})for any other. Themigrate-tokensCLI lives insrc/migrate-tokens.ts.src/master-key.ts—MASTER_KEYandMCP_JWT_KEYresolution (see Auth / tokens).src/auth.ts— OAuth flow + per-account scope resolution: each account'sscopeProfile(from the AccountSet) selects bundles from the catalog;getOptionalBundles()returns the UNION across accounts (drives service registration), authz stays per-account; legacyGOOGLE_OPTIONAL_SCOPESacts as an implicit global profile (warns).src/scope-catalog.ts— the ONE bundle catalog (scopes + description + risk + workspaceOnly; keys are frozen public API, renames via BUNDLE_ALIASES only); unknown bundle names fail E_UNKNOWN_BUNDLE with a did-you-mean hint.src/client.ts—makeGetClient(contextKey, deps)builds a context's client factory;getClientis the owner's. Fresh OAuth2Client per call from the encrypted token (concurrent calls for one alias share the in-flight build); itstokenslistener re-encrypts on refresh (updateToken).CuratedToolDeps(getClientFn,localFiles) is the injection surface for curated tool files.src/accounts.ts—resolveAccounts()builds theAccountSet(registry): whole-registry env override viaGOOGLE_ACCOUNTS, elseconfig.json;getAccountSet()is the live accessor (dispatch-time readers use it, never a captured snapshot);invalidateAccountSet()/isAccountSetStale()back the reload path. Token dir defaults to~/.config/mcp-google-multi/tokens(overrideTOKEN_STORE_PATH).src/config-file.ts—config.jsonzod-strict schema (secrets-shaped keys fail), version gate,mutateConfigFileread-latest-under-lock writer.src/fs-atomic.ts— path-keyed hardlink lock + atomic write (generalized from the token store; token writes delegate to it).src/identity.ts—buildIdentityContext(env, {transport}): the frozen forward-compat seam{subject, accounts, policy, getClient, tokenStore}(subjectis'owner'in the free core);buildRegistry(server, ctx)consumes it.index.tsbuilds one owner context per transport;isOwnerContext()gates host-disk tools and the account wizard.src/migrate-config.ts—migrate-configCLI: env ->config.jsonaccounts (idempotent, never edits env).src/tools/_errors.ts—mapGoogleErrortyped taxonomy +handleGoogleApiErrorresult wrapper; each service file defines its ownhandle<Service>Errorshim over it (optionally with a service-specific 403 hint).src/tools/_coerce.ts—coerceArray/coerceJson/coerceBooleanfor string-encoded client args.src/scope-observability.ts— the #114 three-state classifier (callable / requestable_not_granted / not_requestable{add_bundle|account_type|unknown_scope}): joins a method's required scopes (escape hatch: runtime Discovery; generated tools: baked+interned at gen time onApiMethodRef.scopes) against the account's GRANTED token scopes, enrichesinsufficient_scopehints with copy-pasteable remediation and an honestretriable.account_list's scopes block is the compact profile∪granted report; the registered-tool universe is doctor's. Also exportsaccountScopeSets(granted and profile scope sets for one account;ScopeSetDepsreads a custom token store) andsummarizeToolScopes(how many registered tools an account can call and which bundle unlocks the rest), behind theScopesline ofdoctorandconfig check.src/executor.ts—executeApiMethod: the shared REST executor for generated tools andgoogle_api_call(100k-char response cap).src/arg-normalize.ts/src/arg-strict.ts— transport-level argument handling: snake_case→camelCase key rename (GOOGLE_ARG_NORMALIZE, on by default), unknown-argument screening (GOOGLE_ARG_UNKNOWN=reject|warn|off, defaultreject), andwithValidationEnvelope(SDK schema-validation prose → JSON error envelope).src/http-config.ts—MCP_TRANSPORT(stdiodefault |http|both),MCP_HTTP_HOST/MCP_HTTP_PORT(127.0.0.1:4243),MCP_PUBLIC_URL,MCP_ALLOWED_ORIGINS; canonicalizes the advertised resource URI exactly once.src/http-transport.ts—HttpTransportHost: node:http server, Host/Origin guard, stateless per-request/mcpdispatch, mounted AS routes. Stateless HTTP cannot pushtools/list_changed, so it forcescuratedmode.src/http-confirm.ts:HttpConfirm(the registry'shttpConfirmfield): how an escape-hatch write that needs a human is confirmed over stateless HTTP, where the server cannot ask mid-call. 2026-07-28 revision: an SDKinputRequiredresult (elicit keyconfirm, signedrequestStatebound to the call hash and the subject), served by the second server a lane builds throughServerTarget.modern; the client repeats the call with its answer. 2025 revision, only withMCP_HTTP_CONFIRM_TOKEN(orServerTarget.confirmToken): refusalconfirmation_requiredwith asummaryand a single-useconfirmToken(ConfirmTokens, signed withMCP_CONFIRM_KEY, expiryMCP_CONFIRM_TTL_SECONDS); the repeat call carries it outside the tool schema, and a changed, reused or expired token isconfirmation_invalid. stdio keeps live elicitation. User doc:docs/configuration.md; rationale:docs/internals.md.src/oauth-as.ts—buildAuthServer(): built-in OAuth 2.1 authorization + resource server. Mints its own audience-bound HS256 token for the MCP client, gates on an owner Google login (MCP_OWNER_EMAILS; those Google tokens are discarded), keeps per-alias Google tokens separate, and issues signedalias_reauthlinks.src/mcp-token.ts— JWT/state/code signing, replay guard, rotated refresh store, keyed byMCP_JWT_KEY.src/cimd-cache.ts+src/ssrf-guard.ts— client-ID metadata document fetch (HTTPS only, private ranges blocked, capped, cached).src/oauth-consent.ts— loopback consent on an ephemeral port, shared byauth --accountand the wizard.src/reauth-hint.ts— re-auth hint: the CLI command over stdio, a signed link over HTTP.src/hint-provider.ts— optional process-global remediation-hint provider (setHintProvider/hints()), keyed byHintCasefacts. A single-owner process never installs one, so every call site keeps its literal.src/boot-gates.ts/src/tenant-purge.ts— multi-tenant boot gates and token-purge primitives; the free core never installs or calls them.src/doctor.ts—runDiagnostics(): one engine, two skins (doctorCLI,diagnosetool); also theresetCLI.src/api-probe.ts— doctor's live per-service API-enablement probe.src/tools/account-wizard.ts— elicitation-drivenaccount_add/account_reauth/account_write_config(cud=read viaCUD_OVERRIDES, gated byanthropic/requiresUserInteraction); owner context only.src/client-config.ts— MCP-client detection + idempotent config entry (write-client-configCLI); never inlines secrets.src/setup-prompt.ts— thesetupMCP prompt (Google Cloud Console walkthrough).src/usage-metrics.ts/src/metrics-tap.ts/src/metrics-cli.ts— local usage metrics, off by default (GOOGLE_USAGE_METRICS,USAGE_METRICS_PATH); zero egress (import set allowlisted intests/usage-metrics.test.ts), no identity dimensions (it counts protocol revisions asprotoand clients asname@major.minorasclients, nothing else about a caller);metricsCLI reads the local files. User doc:docs/usage-metrics.md.src/registry-transfer.ts—account export/account import: passphrase-encrypted bundle of config + still-encrypted token files (neverMASTER_KEYor the client secret); refuses a tenant-namespaced store.src/env-load.ts(.envloading, real env wins),src/net-tuning.ts(raises Node's happy-eyeballs connect timeout),src/open-url.ts(best-effort browser launch).- Public API: the
exportsmap inpackage.jsonis the supported surface for hosts that embed the core (MIGRATION-v6.mdlists each subpath);tests/exports-map.test.tspins it. A module a host needs must be declared there, and a deep import intodist/outside the map is unsupported.
Adding a tool
import { <service> as <service>Client } from '@googleapis/<service>';
// inside register<Service>Tools(server, deps: CuratedToolDeps = {}):
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
const getClientFn = deps.getClientFn ?? getClient;
server.registerTool(
'service_action_name', // snake_case, service-prefixed; cud inferred from the verb
{
description: '<one sentence; when to use it>',
inputSchema: {
account: accountEnum.describe('Google account alias'), // always first
// wrap array/object/bool params with coerceArray / coerceJson / coerceBoolean
},
},
async ({ account, /* … */ }) => {
try {
const auth = await getClientFn(account as Account);
const svc = <service>Client({ version: '<v>', auth });
const res = await svc.<resource>.<method>({ /* … */ });
return { content: [{ type: 'text' as const, text: JSON.stringify(res.data, null, 2) }] };
} catch (error: any) {
return handle<Service>Error(error, account as Account);
}
},
);Hard rules:
- Flat
zodinputSchema;accountfirst and OPTIONAL everywhere (sole exception:drive_transfertakesfromAccount/toAccount) — the registry injects the configured default at ONE dispatch site (before write-control and the fan-out parse; not meta-skipped, which is what coversgoogle_api_call); omission with no default returnsE_NO_DEFAULT_ACCOUNT. Coerce array/object/bool inputs — clients send them string-encoded. cudis inferred from the tool name (no manual flag). CUD tools are auto-gated by write-control; never add your own write gate. Fix a misclassified verb inCUD_OVERRIDES(registry.ts). Two sanctioned exceptions self-check viaisAllowed:google_api_call(per-method cud) anddrive_transfer'smoveflag (delete inside a create-classified tool, checked againstregistry.policy).- Wrap handlers in try/catch →
handle<Service>Error(→mapGoogleError); errors return{error, message, hint?, retriable, account}withisError: true. Never embed the raw error /error.config/error.response— token-leak. - Return
{ content: [{ type: 'text', text: JSON.stringify(...) }] }. - Resolve everything through the registry's context:
getClientFnfrom the injectedCuratedToolDeps, account emails fromserver.accountSet(). Never readgetAccountSet(), the token-store module functions or the baregetClientfrom a handler: a registry built over another context would silently use the owner's. A tool that touches the server's disk registers only whendeps.localFilesallows it.
Adding a service
src/tools/<service>.tsexportingregister<Service>Tools(server: ToolRegistry, deps: CuratedToolDeps = {}).- Scopes: always-on →
BASE_SCOPESinauth.ts(breaking — forces re-auth → needs aBREAKING CHANGE:footer); optional → aBUNDLE_CATALOGentry insrc/scope-catalog.ts(OPTIONAL_SCOPE_BUNDLESinauth.tsis derived from it); admin →ADMIN_SCOPES(scope-catalog.ts, re-exported byauth.ts). - Add a
ServiceEntrytoSERVICES(src/services.ts), with anenabled()gate for optional bundles —buildRegistry()picks it up. - Run
npm run gen:coverageto regenerateCOVERAGE.md(never hand-edit it), and update the tool/service counts inREADME.md,docs/features.mdand theserver.jsondescription.
Generated tools (Layer 2)
src/tools/generated/*.tsare EMITTED bynpm run gen:toolsfrom thediscovery/snapshot (npm run gen:discoveryrefreshes it) — never hand-edit; changescripts/gen-config.ts(CURATED_METHOD_IDSskip-list,NAME_/CUD_/BODY_/DESCRIPTION_OVERRIDES) and regenerate.- When a new curated tool covers a Discovery method, append the methodId to
CURATED_METHOD_IDSand regenerate so the generated duplicate disappears;gen:toolsfails on name collisions with curated tools. - Generated tools pass an explicit
cud(derived from HTTP semantics) through the registry config — the only sanctioned use ofconfig.cud; curated tools keep name-verb inference. - They dispatch through
executeApiMethod(src/executor.ts) with method metadata baked at gen time; the escape hatch shares the same executor (no runtime discovery fetch for generated tools). - A new generated-only service needs: a
GEN_APISentry (scripts/gen-config.ts), aBUNDLE_CATALOGbundle inscope-catalog.ts+GENERATED_GATESentry inservices.ts(or ungated when it authorizes via already-granted resource scopes), and aSUPPORTED_APISentry (plusAPI_ALIASESif agents name it differently) insrc/discovery-client.tsfor escape-hatch/policy parity. npm run gen:coverageregenerates COVERAGE.md from the registry — run it whenever the tool surface changes.
Auth / tokens
- Tokens are encrypted at rest — never write plaintext.
MASTER_KEYresolves ONLY throughresolveMasterKey()insrc/master-key.ts(env > keychain > 0600 file > generate; hard guard: never generate while<alias>.encexist).MCP_JWT_KEY(HTTP AS signing key) resolves the same way viaresolveJwtKey(), without the guard. Never read either env var directly, never log key bytes (provenance label only). - Always go through the injected
getClientFn(account)(handles refresh + re-encrypt). Token files are0600. - Secret-injection patterns (why
MASTER_KEYshouldn't live in.envlong-term):docs/secrets.md.
Write-control
Reads are never gated. CUD is deny-by-default: GOOGLE_PROFILE (read-only / safe-writes / full-writes) + GOOGLE_READ_ONLY + GOOGLE_WRITE_ALLOW/DENY globs. Verdict + precedence in write-control.ts, tested in tests/write-control.test.ts. Patterns match each tool as service:cud, service:op and tool:<name> (exact tool name, globs allowed); only tool:<name> singles out a tool named after its own class (tool:drive_update leaves drive_move alone). User-facing env reference: docs/configuration.md.
safe-writes is not "cud != delete": privilegeOf() also refuses privileged-scope writes (admin/Vault/reseller/script) and push registration (*_watch, *_subscriptions_create|reactivate), derived from the method's declared scopes and name so it survives regeneration. Pass scopes on the ToolRef at any new isAllowed call site, or the gate silently sees an unprivileged tool. denyReason() reports WHICH rule refused (readOnly > deny > allow > profile) and the hint is built from it, so never hand-write a hint that lists settings the verdict did not use.
Email compose (gmail_send / gmail_create_draft)
Both route through composeRaw() in gmail-mime.ts (nodemailer MailComposer): header/RFC-2047/address encoding and boundaries are the library's job. A pre-check rejects CR/LF in from/to/cc/subject (E_HEADER_INJECTION). attachments are read by THIS server into buffers (disableFileAccess/disableUrlAccess on MailComposer), absolute-path-only, basename-guarded, files inside the server's config/token dir refused, total capped at Gmail's ~25 MB message limit once encoded (E_ATTACHMENT_TOO_LARGE). Do NOT reintroduce hand-rolled header encoders (they were the injection surface).
Drive specifics
- Every
fileIdcall:supportsAllDrives: true; lists:includeItemsFromAllDrives: true. drive_uploadconvertTo: resource mimeType = targetapplication/vnd.google-apps.*, media = source.drive_exportis the reverse.- Comment text field is
content. Comments/Replies API requiresfieldson every call (see*_FIELDSconstants indrive.ts).
Sheets/Docs field masks
batchUpdate Request types need explicit fields masks — compute from input keys, never wildcards. Helpers buildCellFormat / buildParagraphStyle / buildDocumentStyle are unit-tested in tests/field-mask-helpers.test.ts.
Versioning & releases (automated — CI controls it)
- semantic-release on push to
dev(alpha) /staging(beta) /main(stable). Never bumppackage.json, write a changelog, or tag by hand. - Conventional Commits:
fix:=patch,feat:=minor, aBREAKING CHANGE:footer=major. The analyzer's default (angular) preset does NOT parse a barefeat!:— a breaking commit MUST carry the footer or it releases nothing. A newBASE_SCOPESscope is breaking. - Merge commits only (squash/rebase disabled) — each branch's commits land individually, so keep them clean Conventional Commits.
- After a release,
.github/workflows/backmerge.ymlresyncs the source branch forward: stable =main → staging + dev, beta =staging → dev. Never start new work ondevuntil the post-promotion backmerge has merged.
Testing
npm run typecheck && npm run lint && npm run test && npm run buildbefore any PR. CI also runsnode scripts/measure-tools.mjs --check(lazytools/listbyte budget) andnpm run eval:contract(seeeval/README.md).- Unit-test pure logic (write-control verdict, coercion, error mapper, token-store crypto,
inferCud, field-mask builders). Don't mock the@googleapis/*clients — smoke-test handlers against real accounts.
Don'ts
- No
console.logfrom handlers, and nothing may write to stdout at import time either — stdio is the MCP channel (the dotenv v17 banner corrupted it, which is why env loading is the nativeprocess.loadEnvFileinsrc/env-load.ts; guarded bytests/stdout-purity.test.ts).process.stderr.writeonly. - Don't hardcode aliases — read them from the registry (
server.accountAliases()/server.accountSet()). - Don't bypass
getClient; don't write plaintext tokens or perms wider than0o600. - Don't re-implement error mapping or write gating — use the shared helpers.
- Don't add narrative comments — non-obvious WHY only.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

