Overview
Motif is a unification layer for AI coding agent sessions. It collects what Claude Code, Codex and Cursor write on every machine in your team, keeps it on a server you host, lets any session continue natively in another tool or in a teammate's editor, and exposes the whole thing back to your agents so they stop re-deriving what the team already worked out.
Three layers, in the order you meet them:
- Collect. A small daemon watches where each agent already writes and ships sessions to your server as they happen. Filtering and secret redaction run on the source machine, before anything uploads.
- Move. A session started in one tool becomes a native session in another, or lands in a teammate's editor through their daemon. The target tool opens it as its own history, not as a summary.
- Recall. Sessions become a memory your agents can query over MCP, and that any teammate can question directly: the agent that lived a session answers about it, with full context.
Apache-2.0 · self-hosted · one process and one SQLite file · four runtime dependencies · no telemetry · 103 tests on Linux, macOS and Windows.
Install
Motif needs Node 22 or newer. The npm package is getmotif; the command it installs is motif.
# run it without installing anything $ npx getmotif up # or keep it around $ npm install -g getmotif $ motif up
There is no install script and nothing runs as root. Everything Motif writes on your machine lives under ~/.motif.
Releases are published from CI with an npm provenance attestation, so you can verify the package was built from this repository at that commit.
Quickstart
Solo first. motif up runs the same server locally and syncs this machine into it, so you can see your own history before deciding anything about a team.
$ motif up server http://localhost:4680 ready watching ~/.claude/projects · ~/.codex/sessions · Cursor ● indexed your sessions · watching for new ones
In another terminal, look around:
$ motif scan # what Motif can see on this machine $ motif list # sessions, newest first $ motif search "retry" # full text, across tools $ motif recall "how do we handle webhook retries" $ motif ui # open the dashboard
The dashboard
motif ui opens the web dashboard the server hosts itself. It has five screens: Sessions (everyone's sessions on one thread, filterable by agent, person and project), People, Memory, Search and Setup.
Opening a session shows the transcript as a conversation, with the tool calls folded away. From there you can ask the session a question, pin a note onto it or onto one message, change whether it is team-visible, and continue it in another tool or hand it to a teammate.
Sign in with your member token from ~/.motif/config.json for full access, or the shared team token for a read-only view. Actions like handoff run through your own daemon, so they need the member token.
Setting up for a team
One person runs the server. Everyone else runs connect once and leaves the daemon on. This is the whole thing, including the details that bite.
1 · Someone runs the server
On a machine the team can reach: a company box, a small VPS, or one person's laptop on the office network.
$ docker compose up -d # or, without Docker $ MOTIF_TOKEN=<pick-a-long-random-string> npx getmotif server --host 0.0.0.0
It prints the team token and the exact command teammates should run. Two things are worth deciding rather than defaulting:
- Set MOTIF_TOKEN yourself. Left out, the server generates one and stores it, which works, but you can only recover it from the server's own output or its database, and a fresh database means a different token. Choosing it makes the setup reproducible.
- Bind explicitly. The default is 127.0.0.1, which nobody else can reach. Teammates need
--host 0.0.0.0, plus a firewall rule or a reverse proxy in front.
Sessions, members, tokens and memory all live in one SQLite file: ~/.motif/motif.db, or /data/motif.db in the Docker volume. That file is your team. As long as the server starts against the same file, the team token stays the same across restarts, every member stays connected, and an old invite still works.
Start the server against a different path and you have a new, empty team: new token, no members, no history. Nobody is locked out permanently, the data is still in the old file, but everyone has to reconnect. Pin the path with MOTIF_DB_PATH or use the Docker volume, and back the file up.
2 · Each teammate connects, once
$ npm i -g getmotif
$ motif connect https://motif.internal.yourco.dev \
--token <team-token> --name "Ada" --email ada@yourco.dev
$ motif daemon install # starts at every login
connect mints a member token for that person on that machine and stores it in ~/.motif/config.json. Only its hash reaches the server. Everything they write afterwards is attributed to them; the team token cannot write anything. One person on several machines runs connect on each, and the same --email keeps them one person in the dashboard.
3 · Nothing is shared until someone says so
This is the part people get wrong. Connecting shares nothing. Every session uploads as personal: stored on the server, visible only to its owner.
$ motif projects team ~/work/payments-api # share the whole tree, old sessions included $ motif projects list # what syncs, and how $ motif projects exclude ~/personal --purge # keep out, and withdraw what already went
On a shared or personal machine, start in allowlist mode instead, where nothing syncs until you name it: motif connect <url> --token <t> --name "Ada" --selected, then motif projects include ~/work/company-repo. Promoting a single session by hand sticks: a later re-sync will not undo a choice a person made deliberately.
4 · Check it worked
$ motif status # server, identity, daemon, scope, what was detected $ motif doctor # the same as a checklist, with the fix for each failure $ motif list # teammates' team-visible sessions should appear
If doctor reports that member identity is missing, you are signed in with the team token. Run connect again.
5 · What each token can do
| team token | member token | |
|---|---|---|
| read team-visible sessions, search, recall | yes | yes |
| register a new machine | yes | no |
| upload sessions | no | yes |
| hand off, ask, pin notes | no | yes |
| see their own personal sessions | no | yes |
Give people their own member token, or just have them run motif ui, which signs the browser in for them. The team token gives a read-only dashboard with the action buttons disabled.
6 · Handoff and ask across machines
Both run on the machine that owns the session, through its daemon. A handoff to someone whose daemon is off waits and lands the next time they come online. motif ask on a teammate's session resumes it on their machine, under their own CLI and subscription, so nothing happens until their daemon is running.
$ motif handoff <id> --to-member "Ben" # waits, then tells you when it lands $ motif ask <id> "why did we rule that out?"
TLS
The server speaks plain HTTP and expects a reverse proxy for anything beyond a trusted network. Two lines of Caddy, and teammates connect to the https address:
motif.internal.yourco.dev {
reverse_proxy 127.0.0.1:4680
}
Run a team server
One process, one SQLite file, port 4680 by default. Run it wherever your team can reach it.
$ motif server --port 4680 --host 0.0.0.0 # or, from a checkout $ docker compose up -d
The server prints a team token on first start. Share it out of band; it lets a machine register and read, and nothing more. Set it yourself with MOTIF_TOKEN to keep it stable across restarts.
Motif speaks plain HTTP and expects a reverse proxy in front of it for anything outside a trusted network. A two-line Caddy or nginx block is enough.
Backups
The whole team's history is one file: ~/.motif/motif.db, or /data/motif.db under Docker. Copy it, or point Litestream at it.
Connect a machine
Each developer connects their own machine once, then starts the daemon.
$ motif connect https://motif.your-team.dev \
--token <team-token> --name "Ada" --email ada@team.dev
$ motif daemon start
Connecting mints a member token for that machine and stores it in ~/.motif/config.json. Identity comes from the token, never from a header, so one person on two machines is still one person.
| Flag | What it does |
|---|---|
| --token | The team token from the server. |
| --name | Your name as teammates will see it. |
| Stable identity across your machines. | |
| --selected | Start in allowlist mode: nothing syncs until you include a project. |
Team and personal
Joining a team shares nothing by default. Every session arrives personal, visible only to you, until the project it belongs to is marked team-visible. The boundary is enforced on every read path on the server, not just on the list you happen to be looking at.
$ motif projects list # what syncs, and how $ motif projects team ~/work/payments-api # share this project with the team $ motif projects personal ~/work/side-thing # take it back # stricter: nothing syncs unless you say so $ motif projects mode selected $ motif projects include ~/work/payments-api # or block something, and withdraw what already went $ motif projects exclude ~/personal --purge
Excludes match by path prefix, so excluding ~/personal also excludes everything under it. --purge withdraws sessions that were already synced.
People and tokens
There are two kinds of token. The team token is shared, read-only, and can register a new member. A member token belongs to one person on one machine and can write. The first member to connect becomes the owner.
An owner can rename the team, revoke a member's devices from the People screen, and prune old sessions. Revoking cuts a device off immediately; the sessions it already sent stay.
Recall
Recall answers the question "what does the team already know about this?" and returns a small bundle instead of a pile of history. It is deterministic: full-text search over messages, one hop across the session graph (handoff lineage, shared memory entities, overlapping files), plus distilled notes and anything a human pinned. It packs to a token budget and states why each piece is in there.
$ motif recall "why is the webhook retry capped" $ motif recall "auth middleware" --project ~/work/payments-api --budget 2000
Recall uses no embeddings, no vector store and no model. It is SQLite full-text search and a graph walk, which is why it needs nothing configured and returns the same answer twice.
MCP for your agents
The dashboard is for people. This is the part for the agents: Motif runs an MCP server over stdio, so Claude Code, Codex and Cursor can pull the team's memory themselves rather than being told.
$ motif mcp install # registers with all three $ motif mcp install claude-code # or just one $ motif mcp install --print # show the config, change nothing
Every file it edits is backed up first. The server exposes five tools:
| Tool | What the agent gets |
|---|---|
| recall | The bundle above: what the team already knows about a question. |
| search_sessions | Full-text search across tools and teammates. |
| list_sessions | Recent sessions, filterable. |
| get_session | One session's transcript. |
| ask_session | Put a question to a past session and wait for the answer. |
motif skills additionally installs a Motif skill into Claude Code and Codex, so they know when reaching for the memory is worth it.
Ask a session
Recall retrieves text. Asking is different: the session is resumed read-only on the machine that owns it, behind a preamble that forbids edits, and the agent that lived it answers with everything it saw still in context. Nobody is interrupted, and no transcript changes.
$ motif ask 0d81f3 "what did we rule out here?" $ motif asks 0d81f3 # questions and answers on this session
If the session belongs to a teammate, the request waits for their daemon (--wait, 120 seconds by default) and the answer appears for everyone on the session page. Cursor sessions cannot be asked, because Cursor has no resume-from-transcript command; hand one to Claude Code or Codex first.
Notes and mentions
Notes pin onto a session, or onto a single message inside it, and never modify the transcript. Mentioning someone with @Name resolves against the team roster and reaches them as a terminal notification through their daemon.
$ motif comment 0d81f3 "@Ben this is the bit that broke on Friday" $ motif comments 0d81f3
Session memory
As sessions go idle the server can distil them into entity notes: decisions, files and topics, each with the reasoning attached. A later session that changes an answer marks the earlier note superseded rather than overwriting it, and two notes that disagree are flagged as a conflict so a person sees it.
This is the one part that uses a model, and it is off by default. Point it wherever you like:
$ MOTIF_LLM_PROVIDER=claude-code motif server # uses your local claude CLI, no key $ MOTIF_LLM_PROVIDER=anthropic MOTIF_LLM_API_KEY=… motif server $ MOTIF_LLM_PROVIDER=openai-compatible MOTIF_LLM_BASE_URL=http://localhost:11434/v1 motif server
A daily token ceiling (MOTIF_LLM_DAILY_BUDGET, one million by default) keeps distillation from running away.
Ruling on memory
Distilled memory is a machine's claim about what your team decided. Claims age and sessions contradict each other, so nothing is served as settled until a person says it is.
Every note carries one confidence number, computed deterministically with no model call: corroboration from other sessions and a human's vouch raise it, a conflict, staleness and age lower it. Recall ranks and labels by it, so an agent knows how much to trust each line.
Two things are tracked separately. A note's status is current, superseded or conflicted. Its verification is unverified, verified, disputed or retired. A third flag marks it possibly stale when later sessions reworked its source files without producing a newer note. Human-verified notes are exempt from that heuristic: a person's word is not overruled by a rule of thumb.
$ motif memory review # conflicts, stale notes and disputes, evidence cited $ motif memory prefer 47 --over 12 # this one wins; the loser is superseded, never deleted $ motif memory confirm 31 # vouch for it; verified outranks machine-only $ motif memory dispute 22 # flag it as wrong without ruling yet $ motif memory retire 8 # out of recall, still in the record
Every ruling takes --reason <text>, and the reason is stored with who ruled and over what. An unresolved conflict is shown to agents with both sides and a warning rather than as one quiet wrong answer.
A note is only ever shown when its evidence would be. If the session behind it is deleted, the note keeps the visibility it died with, so removing a personal session can never publish the claims distilled from it.
The Weaver
A ruling fixes the memory. The repository can still say what the losing claim said, and the record can see a change that shipped with no test. The Weaver closes both, on projects you opt in, one at a time, as draft pull requests.
$ motif weaver enable ~/work/payments-api # opt this project in, on this machine $ motif weaver scan # gaps the record sees and the repo has not closed $ motif weaver run src/limiter.ts # queue the missing test for that file $ motif weaver status # what it wove, and what still waits $ motif weaver resolve 14 merged # record a PR's fate: merged or closed $ motif weaver disable ~/work/payments-api # withdraw it
What it will not do
- It works in a throwaway git worktree. Your checkout is never touched.
- It opens a draft PR on a
motif/branch. Pushing to a default branch is not something the code can do. - A repository that already agrees produces no branch and no PR.
- A job born from personal evidence is never queued. What a teammate could not read, the Weaver must not broadcast.
- The agent runs with an explicit allowlist of read, search and edit tools. It gets no shell.
- Claiming a job is atomic with a lease, so two daemons cannot take the same one, and a stale claim returns to the queue.
The server queues; it has no checkout and runs no agent. A daemon holding the project does the work, and the PR body cites the ruling or the session that justified it. Requires a git repository, the claude CLI, and gh with an origin remote to open the PR; without gh the branch is pushed and you open it by hand.
motif weaver resolve <id> closed on a fix born from a ruling sends that ruling back to review. A closed PR is evidence the ruling may have been wrong, so the record learns from what its own hands produced.
The Weave
Everything the sessions form is a graph rather than a list, and the Weave draws it. It is the same graph recall walks to answer inside a token budget, made visible.
Entities are diamonds, sized by confidence; sessions are dots. The edges are relationships the database already holds: a session that informs an entity, a note that contests another, two entities a single session both touched (relates), and a handoff continues lineage. The same idea seen in two projects is folded into one knot, so cross-project ties pull together rather than sitting as duplicates.
Hover any node for its kind, its confidence and how many ties it holds; drag one and the weave resettles around it. The layout is hand-rolled on a canvas with no charting dependency, and it stops computing when it settles. Visibility is enforced exactly as it is for recall: nobody sees a node their evidence would hide.
The Weave draws itself as memory forms, which needs distillation enabled with MOTIF_LLM_PROVIDER. To see it populated immediately, run motif demo.
Try it on an invented team
One command replays a team's week in five acts: sessions stream in, memory distils them, two of them contradict each other, you rule from the terminal, and the Weaver aligns a throwaway repository with your ruling.
$ npx getmotif demo # the whole arc, on port 4699 $ motif demo --fast # no dramatic pauses $ motif demo --auto # do not ask for the ruling, side with the ADR $ motif demo --clean # remove it and exit
No reader runs and your own history is never opened. The repository it builds is real but disposable, and if you rule the other way the Weaver declines to invent work rather than producing a PR for the sake of one.
From the code back to the conversation
Recall starts from a question. blame starts from a file.
$ motif blame src/limiter.ts # the sessions that produced it, freshest first
Each hit names the person, the tool and the session, and motif show <id> opens the conversation behind it. It is the answer to "why is this like this" when you are looking at the line rather than remembering the decision.
What gets collected
Motif reads what the agents already write. It opens those files read-only and never modifies them.
| Tool | Where it is read from | Read | Write |
|---|---|---|---|
| Claude Code | ~/.claude/projects/<project>/<id>.jsonl | yes | yes |
| Codex | ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl | yes | yes |
| Cursor | Cursor's state.vscdb (macOS, Windows, Linux) | yes | no |
Override the locations with --claude-dir, CODEX_HOME and MOTIF_CURSOR_DIR when your setup is unusual. Sync is incremental: only new messages are sent, guarded by a hash of what the server already has.
Handoff
Handoff writes the target tool's own session format and registers it in that tool's own state, so the tool lists it in its resume picker and opens it as its own history. It runs in both directions between Claude Code and Codex; Cursor sessions can be handed to either.
$ motif handoff 4f2a9c # default target: codex $ motif handoff 4f2a9c --to claude-code --open $ motif handoff 4f2a9c --to-member "Ben" # to a person, not a tool $ motif handoff 4f2a9c --dry-run
Then codex resume <id> or claude --resume <id>, which --open does for you.
| Flag | What it does |
|---|---|
| --to | Target tool: codex or claude-code. |
| --to-member | Hand it to a teammate; their daemon materialises it on their machine. |
| --cwd | Map the session onto your own clone when a teammate's path differs. |
| --digest [n] | Compress all but the last n messages (60 by default) into a summary. |
| --open | Launch the target straight into the session. |
| --dry-run | Show what would be written, write nothing. |
Reasoning blocks are dropped, because they are not portable between providers. Long sessions travel as a digest with a verbatim tail. Both are visible in the output rather than hidden.
Search, list, show
$ motif list --project ~/work/payments-api --limit 50 $ motif search "idempotency" $ motif show 4f2a9c --tools $ motif show 4f2a9c --json
Search runs against the server's full-text index when this machine is connected, and falls back to a local scan when it is not. Ids can be given as a short prefix.
The daemon
The daemon watches the agent directories, ships new messages as they land, and carries out handoffs and asks that were requested from the dashboard. It is the only piece that runs continuously, and it does nothing when nothing changes.
$ motif daemon start $ motif daemon status $ motif daemon pause # stay alive, stop shipping $ motif daemon resume $ motif daemon install # start at login (LaunchAgent or systemd user unit) $ motif daemon stop
Its log is ~/.motif/daemon.log, rotated at 5 MB.
Measured on 130 sessions and about ten thousand messages: roughly 57 MB resident for the server and daemon together, a 14 MB database, and effectively no idle CPU.
Status and doctor
$ motif status # server, identity, daemon, sync scope, detected sources $ motif doctor # eight checks, each with the command that fixes it $ motif update # is there a newer release?
doctor exits non-zero when something is wrong, which makes it usable in a setup script.
When something is wrong
Run motif doctor first: eight checks, each printing the command that fixes it. If the symptom is on this list, the cause is almost always the one beside it.
| Symptom | What it usually is |
|---|---|
| teammates cannot connect after a restart | The server started against a different database path. That file is the team; point it back, or everyone must reconnect. |
| member identity missing in doctor | Signed in with the team token, which cannot write. Run motif connect again. |
| a teammate's sessions are invisible | Their projects are still personal. They run motif projects team <path>. |
| a handoff never lands | The recipient's daemon is not running. It is delivered when they next come online. |
| the dashboard buttons are greyed out | Signed in with the team token. Use motif ui, which signs in with the member token. |
| a Cursor session cannot be asked | Cursor has no resume-from-transcript command. Hand it to Claude Code or Codex first. |
Monitoring hooks: GET /api/health for liveness, the daemon log at ~/.motif/daemon.log, and motif status --json for a machine-readable snapshot.
Privacy and retention
Redaction runs on the source machine, before upload, and is on by default. Ten high-precision patterns cover OpenAI and Anthropic keys, AWS access key ids, GitHub tokens, Slack tokens, Google API keys, JWTs, PEM private key blocks and Motif's own member tokens. Add your own with redactPatterns, or turn the defaults off with redactDefaults: false.
Retention is the owner's to set:
$ motif prune --older-than 90 # minimum 7 days; distilled memory survives
There is no telemetry. The only outbound call Motif can make is to the model provider you configure for session memory, and that feature is off until you turn it on.
Configuration
Files
| Path | What it holds |
|---|---|
| ~/.motif/config.json | Server URL, tokens, your name and email, sync mode, include and exclude globs, team projects, redaction patterns. |
| ~/.motif/motif.db | The SQLite database, when this machine runs a server. |
| ~/.motif/daemon-state.json | Sync watermarks, so restarts do not resend. |
| ~/.motif/daemon.log | Daemon log, rotated at 5 MB. |
Environment
| Variable | Effect |
|---|---|
| MOTIF_HOME | Move everything Motif writes somewhere else. |
| MOTIF_PORT | Server port. Default 4680. |
| MOTIF_DB_PATH | Database location. Docker sets this to /data/motif.db. |
| MOTIF_TOKEN | Fix the team token instead of generating one. |
| MOTIF_TEAM_NAME | Name the team at first start. |
| MOTIF_LLM_PROVIDER | anthropic, openai, openai-compatible, claude-code, or off. Default off. |
| MOTIF_LLM_API_KEY | Key for the provider, when it needs one. |
| MOTIF_LLM_BASE_URL | Endpoint for an OpenAI-compatible or local model. |
| MOTIF_LLM_MODEL | Model name for distillation. |
| MOTIF_LLM_DAILY_BUDGET | Daily token ceiling for distillation. Default one million. |
| MOTIF_CURSOR_DIR | Where Cursor keeps its state, when it is not the usual place. |
| CODEX_HOME | Where Codex keeps its sessions. |
HTTP API
Everything the dashboard does goes through the same JSON API, with a bearer token. Live updates arrive over server-sent events.
| Route | Purpose |
|---|---|
| GET /api/health | Liveness, for a load balancer or a check script. |
| GET /api/sessions | List sessions, filtered by project, member, agent and scope. |
| GET /api/sessions/:id | One session with its transcript. |
| GET /api/search | Full-text search with snippets. |
| GET /api/recall | The recall bundle, as JSON or markdown. |
| POST /api/sessions/:id/asks | Ask a session a question. |
| GET, POST /api/sessions/:id/comments | Read and pin notes. |
| PATCH /api/sessions/:id/visibility | Move a session between team and personal. |
| GET /api/members | The roster. |
| GET /api/memory/entities | Distilled decisions, files and topics. |
| GET /api/events | Server-sent events: new sessions, messages, notes. |
Security model
- Two token kinds. The team token reads and can register a member. A member token belongs to one person on one machine and can write. Identity is derived from the token, never from a header a client could set.
- Stored as hashes. Tokens are 192 bits of randomness, kept server-side as SHA-256 hashes and compared in constant time.
- Rate limited. Twenty failed attempts a minute from one address and the server starts refusing.
- Visibility is enforced server-side on every read path: list, search, detail, export, asks and comments all go through the same gate.
- Redaction happens before upload, on the machine that owns the session.
- Put TLS in front. Motif speaks plain HTTP and expects a reverse proxy for anything beyond a trusted network.
Found something? Write to mert@getmotif.dev rather than opening a public issue.
Uninstall
$ motif uninstall # stop the daemon, remove the login item $ motif uninstall --purge # also delete ~/.motif
Your agents' own session files are never touched. Remove Motif and Claude Code, Codex and Cursor carry on exactly as before.