Skip to main content
Glama

todox

Working memory for developers and their agents. Not a checklist — a log your next session can actually resume from.

ci licence: MIT live

An issue tracker is written human-to-human. todox is written agent-to-agent, with a human reading over its shoulder. Every task carries the decisions behind it, the approaches that failed, the questions still open, and the note the last session left behind.

A fresh agent calls get_context, reads that, and starts where the last one stopped — without walking into a wall somebody already hit. The briefing is capped rather than unbounded, in rows and in bytes, and it reports what the caps left out instead of trimming in silence. Nothing is ever cut mid-sentence: every record comes back named and dated with its first line, and a body of null means the budget was spent, not that the record is empty.

What goes in a log

kind

what it means

decision

what you chose, and why the alternatives lost

dead_end

an approach that did not work — the highest-value entry, because it stops the repeat

question

something only a human can answer

handoff

end-of-session state, written for a stranger

note

everything else

Two things fall out of treating the log as the product:

  • Stale context is flagged, and never faked. Linked files are hashed by the side that can see them — the agent — and the server stores the hashes and compares. If the code moves on, get_context says the note may be lying. Until an agent has actually looked, the note is marked as never checked rather than claimed to be fresh: context that lies is worse than none, and that includes lying about how sure we are.

  • Reports come from the log, not from commits. Every status change is an event, so what did I finish today, how long did it take, which model did it is a query rather than archaeology. A task set doing and walked away from counts as the morning it was set, not the weeks since -- and the report says how much it left out, rather than folding it into the headline.

  • A file can be asked what is known about it. The same links that carry the hashes are readable from the other end: get_file_context takes a path and answers with the tasks that touched it, their dead ends, and any standing note attached to it. Paths are folded to their repo-relative form, so a link made on one machine is found from another.

Related MCP server: AgentDrive MCP Server

Why not the memory your agent already has?

Claude Code writes its own notes now (auto memory, on by default since February 2026), and Cursor and Codex have theirs. Use them: they are good at remembering that you prefer pnpm. What their documentation says, in its own words, is that they are per repository, per machine ("machine-local … not shared across machines") and per tool, and shared with nobody ("just you"). todox is for what falls outside that line:

  • Two machines, one log. A repository is identified by its remote, so the note left on the laptop is read on the desktop.

  • Every agent, one log. Claude Code, Codex, Cursor and VS Code all speak MCP; the handoff one leaves is what the next one reads, whichever it is.

  • The people, too. A project can be shared, and the log carries who wrote what and which model did it.

  • A shape a note in a file does not have. Tasks that open and close, dead ends as their own kind, a report read from the log, a stale note that says so — and a briefing that reports what its caps left out.

Try it

todox.dev — anyone can register. Small personal deployment, no uptime promise. Self-host if the log matters to you.

Run your own

pnpm install
cp .env.example .env.local     # any Postgres 15+; see below for a container
pnpm db:migrate                # idempotent
pnpm seed                      # optional demo account: demo / todox-demo
pnpm dev

Connect an agent

todox is a remote MCP server. There is nothing to install and no repository to clone: point any MCP client at the URL with an agent token.

Create a token on the Account page and it hands you text you can paste straight into whichever agent you use, plus the config snippet for the four common ones. The shape is always the same — one URL, one header:

Claude Code has a shorter path. The plugin carries the server, the session protocol as a skill, and one reminder at session start; it asks for the token once and keeps it in your Claude Code settings:

claude plugin marketplace add beydemirfurkan/todox
claude plugin install todox@todox

Everything else below still applies to it — the plugin is the same four lines, installed rather than pasted. See plugin/README.md for what it does and does not do.

# Claude Code. --scope user, because the default is this directory only.
claude mcp add --scope user --transport http todox https://www.todox.dev/api/mcp \
  --header "Authorization: Bearer todox_…"
// OpenCode v1 — ~/.config/opencode/opencode.json.
// MCP key is `mcp` (server name is a direct key under it), NOT `mcpServers`.
// `type` is `"remote"`, NOT `"http"` — the Claude/Cursor/VS Code value is
// silently ignored on OpenCode.
{
  "mcp": {
    "todox": {
      "type": "remote",
      "url": "https://www.todox.dev/api/mcp",
      "headers": { "Authorization": "Bearer todox_…" }
    }
  }
}
// OpenCode v2 — same key, server now nested under `mcp.servers`.
{
  "mcp": {
    "servers": {
      "todox": {
        "type": "remote",
        "url": "https://www.todox.dev/api/mcp",
        "headers": { "Authorization": "Bearer todox_…" }
      }
    }
  }
}
# Codex — ~/.codex/config.toml
[mcp_servers.todox]
url = "https://www.todox.dev/api/mcp"
http_headers = { Authorization = "Bearer todox_…" }
// Cursor — ~/.cursor/mcp.json, the one in your home directory.
{
  "mcpServers": {
    "todox": {
      "type": "http",
      "url": "https://www.todox.dev/api/mcp",
      "headers": { "Authorization": "Bearer todox_…" }
    }
  }
}
// VS Code — the user-level mcp.json ("MCP: Open User Configuration").
// The root key is "servers", NOT "mcpServers". This is the one client
// that differs, and getting it wrong is silent.
{
  "servers": {
    "todox": {
      "type": "http",
      "url": "https://www.todox.dev/api/mcp",
      "headers": { "Authorization": "Bearer todox_…" }
    }
  }
}

The MCP config key and the type value differ per agent, and the wrong combination is silently ignored — no error, no warning, the tool just does not show up:

agent

key

type

Claude Code

mcpServers.NAME

"http"

OpenCode v1

mcp.NAME

"remote"

OpenCode v2

mcp.servers.NAME

"remote"

Cursor

mcpServers.NAME

"http"

VS Code (Copilot Chat)

servers.NAME

"http"

Codex

TOML [mcp_servers.NAME]

n/a

Where those files live differs by platform, and VS Code is the one that is not where a Linux habit puts it:

agent

macOS

Linux

Windows

Claude Code

~/.claude.json

same

same

Cursor

~/.cursor/mcp.json

same

same

Codex

~/.codex/config.toml

same

same

OpenCode

~/.config/opencode/opencode.json

same

same

VS Code

~/Library/Application Support/Code/User/mcp.json

~/.config/Code/User/mcp.json

%APPDATA%\Code\User\mcp.json

Install it globally, not per project. Every one of these tools defaults to the directory you are standing in — claude mcp add without a scope, .cursor/mcp.json, .vscode/mcp.json — and a memory that only exists in one repository is the opposite of the point. It also fails quietly: the tools simply are not there in the next project, so the agent never mentions them.

Spell out "type": "http". A client that finds a url without one tends to assume a local command and fails with something unhelpful.

Then tell your agent to use it

Connecting is not the same as being used, and the gap is bigger than it looks. An MCP server's instructions are background reading; a skill or a CLAUDE.md rule is an instruction. When they disagree, the server loses — measured, in a fresh project, with todox connected the whole time and never once called.

So put four lines in the memory file your agent actually obeys:

todox MCP is installed here — persistent memory across projects.

- Call `get_context` before starting non-trivial work (cwd = your working
  directory). It registers a new repo by itself.
- `create_task` for anything that will not finish this session.
- Before stopping, `session_status` lists what you touched; leave a
  `log_entry(kind:'handoff')` on each, and `dead_end` for approaches that failed.
- Always pass your own model id.

Or let the installer do it:

pnpm install:mcp claude-code --write-memory

It is off unless asked, because that file is yours rather than ours, and it is idempotent — the block is fenced with an HTML comment, so a second run replaces it instead of leaving two sets of instructions where the older one wins. Add --dry-run to see the exact block first.

When the tools do not show up at all, the silent failure is usually one of the two tables above, and there is a command that reads the files the way each client does and says which:

npx https://github.com/beydemirfurkan/todox/releases/latest/download/todox-mcp.tgz doctor

Per client: the entry, its type, its root key, a leftover in a location the client never reads, whether the memory file carries the habit, and whether the server answers the token it found (masked in the output). It also names a todox entry sitting in the current checkout's own config for what it is. Nothing is changed; docs/mcp.md has the detail.

If you skip this and the account then connects for days without calling a tool, the server says so at the top of its instructions on the next session — "connected for N days and has not called a tool" — and hands the agent the four lines and the path of your client's memory file, telling it to append them and to tell you it did. Measured from tool_usage, so it is said only while it is true and stops the moment a tool is called. That is how an account whose client changed, or whose memory file was never written, comes back without anybody having to ask.

The user-level file, not the project one. This is the same trap as the config above, one directory over:

Agent

The file that applies everywhere

Claude Code

~/.claude/CLAUDE.md

Codex

~/.codex/AGENTS.md

Cursor

~/.cursor/rules/todox.md

VS Code

~/.copilot/instructions/todox.md

OpenCode

~/.config/opencode/AGENTS.md

A repository's own AGENTS.md, and the per-project rules files the editors also read, apply inside that checkout only. A cross-project memory installed into one project is the thing this whole section exists to avoid.

The longer version, as a skill. The four lines are the habit; the whole session protocol — the same text the server sends at initialize — can also sit in the client's user-level skills directory, where every one of these clients loads a SKILL.md by its description when the moment matches, and spends nothing on it otherwise:

pnpm install:mcp claude-code --write-skill

Agent

The skill file it loads

Claude Code

~/.claude/skills/todox/SKILL.md

Codex

~/.agents/skills/todox/SKILL.md

Cursor

~/.cursor/skills/todox/SKILL.md

VS Code

~/.copilot/skills/todox/SKILL.md

OpenCode

~/.config/opencode/skills/todox/SKILL.md

The file is wholly todox's — a directory of its own, nothing of yours inside — so a second run after an upgrade replaces it. It is generated from the same text as the server instructions rather than written twice, which is what keeps the two from disagreeing.

The token stays out of that file — it lives in your MCP config. This is the habit, not the credential.

Optional: local mode

The hosted server has no filesystem — but your agent does, and that is enough: it sends the hash when it links a file and calls report_file_hashes with what it finds afterwards, so staleness works over HTTP like anywhere else.

The stdio server does that part itself rather than asking. Worth running if you would rather not spend an agent's attention on it, or want the hashing to happen even when the agent forgets. There is nothing to clone:

TODOX_TOKEN=todox_… TODOX_URL=https://www.todox.dev \
  npx https://github.com/beydemirfurkan/todox/releases/latest/download/todox-mcp.tgz

Or as an MCP config, which is the form an agent wants:

{
  "mcpServers": {
    "todox": {
      "command": "npx",
      "args": [
        "-y",
        "https://github.com/beydemirfurkan/todox/releases/latest/download/todox-mcp.tgz"
      ],
      "env": { "TODOX_TOKEN": "todox_…", "TODOX_URL": "https://www.todox.dev" }
    }
  }
}

There is no npm package, and that is a decision rather than a to-do. A GitHub Release needs no account and no token to publish or to install from, so the tarball is the whole distribution and npx takes its URL directly. The URL above always resolves to the newest release; every release also carries a todox-mcp-<version>.tgz if you would rather pin and choose when to move.

It carries only what the stdio server actually loads — no Next, no React, no Postgres driver, because it talks to the API over HTTP and never opens a database. pnpm pack:mcp builds it, and fails the build if anything server-side ever finds its way into the tool surface again.

From a clone, pnpm -C /path/to/todox mcp still works and is what to use when you are changing the tools themselves.

Tools

tool

what it does

get_context

Call this first. Standing rules, project decisions and gotchas, every open task with its decisions, dead ends, questions, files and last handoff — plus stale-file warnings. Resolves a project from a slug, a name, or any path inside it. Capped in rows and in bytes, never truncated: every record keeps its id, kind, date and first line, and a body of null means the budget was spent — get_task reads it. Open tasks untouched for 14+ days come back as a line each in idle_tasks — title, status, days idle, last handoff head — outside every budget. Pass focus — a sentence about what the session is for — and both budgets are spent on the records that answer it rather than the newest ones, which is what lets them be smaller.

create_task

Capture work. Pass cwd or an explicit project; the tool schema requires one before the call runs. Registering a new one also needs repo_root or repo_url. The receipt returns the task path and body length without echoing the body.

update_task

Status, title, body, priority. Moving to doing/done is where durations come from.

log_entry

Append one of the five kinds. answers_entry_id closes a question — the only thing that does.

delete_entry

For an entry that was wrong when it was written. One overtaken by later work is history, not an error — append instead.

activity_report

Today / this week / any window: durations, models, importance, decisions, dead ends, open questions. format:"markdown" is written to be pasted into a status update.

link_files

Attach paths with their hashes to a task or a context note — the files the work touches, and the plan it follows, wherever that lives: a path outside the repository is hashed like any other, a URL (a claude.ai artifact, say) is kept as written and never hashed. Safe to call again for the same file.

report_file_hashes

Hosted only: what the linked files look like on disk now. The local process does this for itself.

accept_file_change · unlink_file

Clear a stale warning once you have read the change, or drop a link that has stopped meaning anything. Nothing else can clear it — the server never sees the file.

add_context

Knowledge that outlives a task; omit the project to make it account-wide.

get_context_note

One note in full, for the ones whose body the briefing capped and for reading past a search snippet. An entry the budget did not reach is read with get_task.

get_file_context

What is known about one file: the tasks that touched it with their dead ends, and the notes attached to it. Absolute or repo-relative; both find a link made on another machine.

session_status

Call before finishing. The tasks you changed or wrote on in this project within the last 12 hours (any status), each with whether a handoff was written since the last thing you did there — plus tasks left doing for 7+ days with nothing logged. Ids, titles, dates and one boolean; the hint says what each list still needs. The wrap-up rule asks for a handoff on every task touched, and this is the list it assumed the agent would remember.

update_context · delete_context

Correct a note that turned out wrong. A log that can only be added to stops being worth reading.

search

Across all your projects, ranked by relevance. Ask the question in words; quote a phrase to require it. Stems English and Turkish, and still matches the middle of an identifier. Words that only one of the two languages treats as noise are dropped, so a question does not match every record containing the word "a". kinds narrows to dead ends or decisions; project stops it looking elsewhere. Ask in the language the log is written in — it stems, it does not translate — and if a sentence finds nothing, retry with the distinctive identifiers alone (3+ characters; shorter ones match as whole words only).

get_task

One task with its log and linked files.

list_tasks · list_projects

The plain lists, when get_context is more than you need. Projects are newest-activity-first and carry activity_at.

create_project · update_project

Rarely needed: create_task with a cwd registers one. A summary is worth adding.

delete_project

The way back from a mistyped cwd. Takes the project and everything under it; confirm must be the slug.

merge_projects

The way back from one repo registered twice. Moves tasks, notes and paths into the surviving project; confirm must be the slug of the one being merged away.

Every write tool takes a model, and the server instructions tell the agent to always pass it. That is what makes the per-model breakdown real rather than guessed.

Prompts

Three, because there are three moments this is for. They show up in your client's own menu, so you can see what the server does without reading anything:

prompt

when

start_session

before planning — read what earlier sessions established

wrap_up

before finishing — leave a handoff, and the dead ends especially

standup

when somebody asks what got done

Deploying

A container and a Postgres beside it. docker-compose.yml at the root is that, assembled — the database publishes no port at all and is reachable only over the compose network:

cp .env.example .env       # set POSTGRES_PASSWORD and TODOX_PUBLIC_URL
docker compose up -d --build
docker compose exec app pnpm db:migrate

The migration is a separate line on purpose; see the note at the end of this section. todox.dev itself runs the same two containers on one host.

variable

why

DATABASE_URL

Postgres. When the database is a neighbour on the same network this is its service name, and no certificate or public port is involved.

DATABASE_POOL_MAX

Optional, default 10. Connections this process may hold. Raise it only after checking the server's own max_connections, which every replica shares.

TODOX_PUBLIC_URL

Verification links, reset links and the agent setup snippet are built from it — get it wrong and people, and their agents, land on the wrong host.

SMTP_HOST · SMTP_USER · SMTP_PASS · MAIL_FROMSMTP_PORT)

Optional, but the first four together. Without them mail is printed to the server log rather than sent. Port defaults to 587 (STARTTLS). What MAIL_FROM may be depends on the provider: a mailbox provider usually wants the address that authenticated, while an API-key provider wants any address on a domain verified with it. If a sending limit is hit, messages are dropped and the failure shows up only in the log.

Run pnpm db:migrate when the schema changes. It deliberately does not run at startup: DDL racing between instances of a rolling deploy is a bad way to discover lock contention, and the schema is idempotent precisely so the decision can be made after a deploy rather than during one. From the host:

docker exec <container> pnpm db:migrate

That is also why the image keeps its dev dependencies instead of using Next's standalone output — pruning them removes tsx and everything under scripts/, and a database that is deliberately unreachable from the internet can only be migrated from something already inside the network.

Knowing it actually deployed

A merge is not a deploy, and the gap between them is silent: nothing errors, the site stays up, and the only symptom is that a fix you watched go green is not the one people are running. On 2026-09-05 this instance served code two days and fifty-six commits old, and the thing that made it visible was looking rather than anything reporting it.

The image tag is the git sha and the container name changes on every deploy, so one line answers it:

docker inspect <container> --format '{{.Config.Image}}'

Compare that against git rev-parse origin/main. If they differ, the code you are reading is not the code that is running.

Automatic deploys are worth wiring up, and worth checking after you do. A platform that pulls on a webhook can have the switch on and still never fire, because the switch and the webhook are two settings in two places: this instance had auto-deploy enabled for weeks while the repository had no webhook at all, so nothing was ever told to look. After wiring one, confirm from the sending side — the delivery log — rather than from the switch, because a webhook pointed at the wrong path answers cheerfully and does nothing.

Taking your data with you

The Account page has a Download my data button, and /api/export answers the same file to a bearer token — so an agent can write the backup without the result passing through a model. It carries every project you own with its tasks, log, context notes and file hashes, and nothing about anybody else: no credential, no collaborator, no share token, and no projects that were shared with you, which belong to whoever made them.

Loading one into an instance you run:

pnpm db:import ./todox-export-2026-08-18.json your-username

Additive, never destructive: nothing is deleted or overwritten, and a project whose slug is taken arrives under the next free one. Task events come across too, so durations in a report on the restored copy say what they said on the original.

Security

Passwords are scrypt; sessions, agent tokens and email links are stored as hashes only. Ownership is enforced in one module, and a row belonging to someone else answers 404 rather than 403 so ids cannot be probed. Rate limits live in the database, so they hold across instances.

Details, and an honest list of what is not covered, in SECURITY.md.

Known gaps

  • Search's substring half is indexed only where the database allows it. The two halves are asked separately and merged, which is what lets either use an index at all — measured on 110k rows, a search went from 5.7s to 0.16s on the full-text arm alone. The ILIKE arm that finds identifiers full-text cannot needs pg_trgm; pnpm db:migrate creates the extension and its five indexes where the role may, and says so either way. Where it may not — a Postgres without contrib, a managed service that refuses extensions — that arm stays a sequential scan and pnpm smoke:search prints SKIP for it rather than failing. The postgres:18 image in docker-compose.yml has it.

  • Staleness is per-file hash; per-symbol would be the honest version. Hosted, it depends on the agent actually sending hashes — the instructions ask, and nothing can make it.

  • Coverage sits around 39%, and the shape matters more than the number: the agent surface, the auth boundary and the repositories that answer "is this yours" are covered, while much of the UI is not.

  • Observations see what git can tell them and which task a session set to doing, so they answer "what changed" and "on what" — and never "why". The half that carries reasoning is a transcript, and the only hook API that exposes one belongs to a single client.

  • The briefing's byte budget covers log bodies, note bodies and task bodies. One axis is still bounded only by a row count: the heads of carried entries and tasks, fifty tasks' worth. It is far smaller than what the budgets fixed — one project went from 143 KB to about 55 KB on the log alone — but it is not bounded in bytes, and pnpm bench:memory prints it so the next reader does not have to discover it.

  • Observations are captured by the local process only. Watching git means running on the machine that holds the checkout, and the hosted endpoint has none — so connected that way, the observations section of every briefing stays empty and nothing anywhere fills it. The Try-it path in this README is the hosted one, so that is most people. npx todox-mcp is the transport that captures. Everything else works identically either way.

  • No 2FA, no per-session revocation, no audit log.

  • Share links are unlisted, not access-controlled.

  • No keyboard navigation beyond / for search.

Cutting a release

git tag v0.1.4 && git push origin v0.1.4

That is the procedure. The workflow checks the tag against package.json, runs the checks, builds the stdio package and attaches it to a GitHub Release — no account and no credential involved, so npx <that tarball url> works from the first tag.

Two names go up: todox-mcp-<version>.tgz, and the same bytes as todox-mcp.tgz so that /releases/latest/download/todox-mcp.tgz is an address worth writing into a config once. Nothing is published to npm, on purpose — see the local-mode section above.

server.json pins the MCP registry entry to the same version and server-json.test.ts holds it there, so the tag, the package and the registry move together or the release stops.

Contributing

The rules the codebase actually follows, and how to run the checks: CONTRIBUTING.md.

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers