Carry a context pack between your Claude surfaces. Push your voice rules, system facts, or style guide from Claude Code at your desk; read them from Claude mobile on the go. One small remote MCP server, always live, no manual re-sync.
carry exists because there is no public API to write Claude Project knowledge, so keeping your mobile Claude in sync with the instructions you maintain in a repo means either re-uploading files by hand (which drifts the moment you do) or this: a server both surfaces attach to, holding one current pack per namespace.
Claude Code (your repo) Claude mobile / web / desktop
compiles + pushes the pack connector attached once via web
│ │
│ push_context (write token) │ get_context (read: token or OAuth)
▼ ▼
┌─────────────────────────────────────────────────────┐
│ carry — the current pack for your namespace │
└─────────────────────────────────────────────────────┘
Because Claude's remote MCP connectors are account-brokered (you add the URL once
on claude.ai web and it appears on your phone, desktop, and web on the same
account), every surface reads the live pack. Update the pack from your desk, and
the next get_context anywhere has it.
carry holds one current pack per namespace — context you author, not memory it accumulates. Good packs are things you want identical on every surface:
- Voice, style, and brand. Draft on your phone in the same voice your terminal writes in — tone, banned words, formatting rules.
- Coding standards. Your
CLAUDE.mdconventions — stack, naming, testing style, "never do X" — on hand when you ask Claude about code from any device or repo. - Project context. Current architecture, the domain glossary, key decisions, so any surface answers with your project's facts instead of generic ones.
- Who you are. Your bio, stack, and tools, so you stop re-introducing yourself at the start of every mobile session.
- A team's house rules. One shared read pack — API conventions, style guide, onboarding facts — that everyone's Claude reads the same way.
- Slash commands & Claude config, across machines. Push your
.claude/commandsfiles, thencarry pullthem onto another machine — your command library andCLAUDE.mdtravel without a git remote (see the CLI below).
What it is not for: fast-changing, per-conversation memory. carry is a deterministic pack you control, not an accumulating memory store.
Because those are local and single-surface. A slash command lives in .claude/commands
on one machine, and CLAUDE.md is read only by Claude Code — your phone and claude.ai
never see either. carry is the cross-surface, single-source layer they can't be: one
pack you update at your desk and read on demand from mobile, web, desktop, and the CLI.
On Claude Code alone you may not need it; carry earns its place the moment you leave the
terminal.
npm install
npm run demoBoots a throwaway in-memory server, pushes a sample pack with the write token, and reads it back with the read token — the whole loop on localhost, nothing written to disk.
carry is deploy-your-own. There is no shared "carry cloud" and no maintainer
footing a bill for your usage. You stand up your own instance (a one-click Render
Blueprint is included), set your own tokens, and own your data. The optional OAuth
mode uses your authorization server (e.g. your own free-tier WorkOS AuthKit
tenant) — again, your account, not the maintainer's. Running it costs you only
what your host charges (often $0 to kick the tires; a few dollars a month for a
persistent disk). See docs/deploy.md.
Stateless Streamable HTTP on POST /mcp (a fresh MCP server per request; no
session state to leak). Plus GET /healthz.
get_context— return the current pack for your namespace. Read this before drafting.push_context— replace the pack. Requires a write token.carry://context— the same pack as an MCP resource.
carry supports two authentication paths. Static tokens are the default; OAuth is opt-in and off unless you configure it.
1. Static bearer tokens (default — the write path, and a simple read path).
A namespace owns two secrets: a read token and a write token. They must
differ, so a leaked read connector can never overwrite your pack. The token is
the namespace — the model never supplies one, so it can't touch anyone else's
pack. This is all you need for Claude Code (push with the write token) and for any
MCP client that lets you paste an Authorization: Bearer header. No external
service required.
2. OAuth 2.1 protected-resource mode (optional — for the claude.ai connector).
The claude.ai web connector dialog is OAuth-only on personal accounts (there's
no field to paste a bearer read token). To attach carry as a mobile/web/desktop
connector, enable OAuth: carry becomes an RFC 9728
protected resource that validates JWTs (issuer + audience + expiry) against your
authorization server's JWKS. OAuth callers are granted read scope only —
pushing always requires the static write token. OAuth is enabled solely by setting
CARRY_OAUTH_ISSUER; leave it unset and nothing about the static-token behavior
changes. See docs/connector-auth-spike.md for
why this path exists.
Lock it to yourself (OAuth mode). By default, carry accepts any user who can authenticate to your configured authorization server / tenant. For a single-user self-host that tenant is just you, so this is usually fine. If your tenant has other users, set an allowlist so only you can read the pack:
CARRY_OAUTH_ALLOWED_SUBSand/orCARRY_OAUTH_ALLOWED_EMAILS(comma-separated; a caller passes if itssubor
npm install
cp .env.example .env # then set CARRY_NAMESPACES with your own tokens
npm run dev # starts on :8080 (tsx watch)Verify:
npm run smoke # storage + auth cores, no network
npm run typecheck
npm test # full vitest suitecarry ships a small CLI (bin: carry, or node dist/cli.js after npm run build)
that compiles one or more Markdown files into a single pack, stamps its metadata
(source, title, gitHash, builtAt), and pushes it:
# Build the CLI once
npm run build
# Push a pack (write token from $CARRY_WRITE_TOKEN or --token)
node dist/cli.js push --url https://your-instance.example.com/mcp \
--from ./context/identity.md ./context/voice.md --title "My Claude context"
# Read the current pack back (read/write token from env or --token)
node dist/cli.js get --url https://your-instance.example.com/mcp
# Pull the pack back into files — the inverse of push. Splits by source marker and
# writes each file into the target dir. Sync your commands/config across machines.
node dist/cli.js pull --url https://your-instance.example.com/mcp --to ./.claude/commandsTokens are read from CARRY_WRITE_TOKEN / CARRY_READ_TOKEN in the environment
(never passed on the command line unless you use --token). carry pull reduces every
source name to a basename, so a pack can never write outside your target directory.
All configuration is via environment variables (see .env.example):
| Variable | Required | Purpose |
|---|---|---|
CARRY_NAMESPACES |
yes | JSON array of {namespace, readToken, writeToken}. Read ≠ write. The single source of auth. Server fails loudly on bad config. |
PORT |
no | HTTP port. Default 8080; hosts like Render inject it. |
CARRY_DB_URL |
no | Durable storage. Unset = in-memory (dev; lost on restart). file:/data/carry.db for an on-disk SQLite/libSQL file, or libsql://… for hosted Turso. |
CARRY_DB_AUTH_TOKEN |
no | Auth token for a hosted Turso libsql:// URL. |
CARRY_RATE_LIMIT_PER_MIN |
no | Fixed-window limit on POST /mcp, keyed by IP and token. Default 120. |
CARRY_OAUTH_ISSUER |
no | On-switch for OAuth mode. Your authorization server / AuthKit domain (JWT iss). Unset = OAuth disabled. |
CARRY_OAUTH_AUDIENCE |
if issuer set | carry's canonical MCP URL, registered as the OAuth resource indicator (JWT aud). |
CARRY_OAUTH_NAMESPACE |
if >1 namespace | Which namespace OAuth callers map to (read scope). Defaults to the sole namespace when there's only one. |
CARRY_OAUTH_JWKS_URL |
no | Override for the JWKS endpoint. Defaults to ${issuer}/oauth2/jwks. |
CARRY_OAUTH_ALLOWED_SUBS |
no | Lock-to-user allowlist by token sub (comma-separated). Unset = any authenticated tenant user. |
CARRY_OAUTH_ALLOWED_EMAILS |
no | Lock-to-user allowlist by email claim (comma-separated, case-insensitive). A caller passes if its sub or email is listed. |
The included render.yaml Blueprint stands up a Docker web service
with a persistent disk for the libSQL file. Step-by-step (fork → tokens → deploy →
attach the connector) is in docs/deploy.md; day-two operations
(token rotation, backups) are in ops/runbook.md.
- Lock-to-user — allowlist a specific
sub/email in OAuth mode (closes the known limitation above). - Multi-tenant — namespace provisioning + per-namespace tokens in a DB behind
the
ContextStoreinterface, minimal signup, isolation tests. - An accumulating memory layer as an alternative to a single deterministic pack.
TypeScript (strict, ESM, Node ≥20). Express entry, the official MCP SDK's Streamable
HTTP transport in stateless mode, a ContextStore interface with in-memory and
libSQL implementations selected by env, structured per-request logging (never logs
tokens or content), and a fixed-window rate limiter. Auth is a small, separately
tested module (static tokens + optional jose-backed JWT verification). The test
suite runs offline (injected local JWKS for the OAuth path).
Issues and PRs welcome — see CONTRIBUTING.md.
Apache-2.0. See LICENSE.