# Thousand, for AI agents

You were pointed at a Thousand workspace — a team's documentation, kept in git,
served with path-level access control. This page is the entire manual. It is
always at `/AGENTS.md` on the same host, and everything on it works with the
tools you already have: git and HTTP.

Two ways in. **Git** when you work in a filesystem (Claude Code, Codex,
Cursor): clone the workspace, edit files, push. **The agent API** when you were
given an agent key, or when you only need to read, comment, or check access.

## MCP — the one-command way

If your client speaks MCP, everything below is packaged as a server with
browser sign-in built in:

    claude mcp add --transport http thousand https://<host>/mcp
    codex mcp add thousand --url https://<host>/mcp

First use opens the browser: sign in, approve, done. The tools are
workspace_info, list_documents, search, read_document, write_document,
diff_document, comment, delete_comment, audience, check_access,
grant_access, connect_source, create_workspace — same rules
as everything below, and `workspace_info` hands you the git clone URL and
the shape of a document link when you want either. The token it stores is a personal token, revocable on /tokens.

No MCP in this session? Servers attach only at session start, so carry on
with the device sign-in below — but tell your human the one command above
makes every future session smoother.

## Getting in

One credential does everything: a **personal token** (`td_…`). It works as
the git password AND as the `key` in every API call below. It is your
identity: you act as the user it belongs to, in their workspace, under their
access.

**Get one without leaving the conversation** (device sign-in — the
`gh auth login` shape). Start:

    POST /api/device/start            → {user_code, device_secret,
                                         verification_url, interval}

Show your human the `user_code` and `verification_url`, and tell them:
"open this link, sign in, approve the code, come back." Three things that
decide whether this takes ten seconds or ten minutes:

- **If you run on their machine** (a CLI agent with a shell), open it for
  them rather than making them copy a URL: `open <url>` on macOS,
  `xdg-open <url>` on Linux, `start <url>` on Windows. Show the code anyway —
  they must confirm it matches what you displayed. Pasting the URL into a
  shell instead of a browser is a common miss; `?` is a glob character there.
- **Do not try to approve it yourself** in a headless or sandboxed browser.
  Approval needs their signed-in session, and your browser is not theirs —
  signing in would mean handling their password, which you must never do.
  **Open `verification_url` in their default browser yourself** when you
  can (`open <url>` on macOS, `xdg-open <url>` on Linux, `start <url>` on
  Windows) — the prompt should appear in front of them, not be a link
  they have to go find. Show the link too as the fallback, then poll.
  The URL carries only the short code, never the secret, so opening it
  is safe.
- **Keep the `device_secret` outside any page you navigate.** It lives for
  one exchange only, and losing it means starting over with a new code.

Then poll every `interval` seconds:

    POST /api/device/poll
    {"device_secret": "..."}          → {"status": "pending"}
                                      → {"status": "approved", "token": "td_…",
                                         "user": …, "workspace": …,
                                         "git": <clone URL, no token in it>}

Codes expire in 15 minutes. The token appears on their /tokens page as
"agent (device sign-in)", revocable any time.

**Or the manual way:** your human visits `https://<host>/tokens`, mints a
token, and pastes it to you. Same credential, same powers.

There is nothing else to set up. No SDK, no OAuth for you to drive.

Once you hold a token, `GET /api/agent/workspaces?key=td_…` (or the
`workspace_info` MCP tool) lists every workspace your human belongs to —
the same list their sidebar switcher shows. The token itself acts only
in the workspace it was minted in: to work in another one, run the
device flow again from that workspace's host path, or ask for a token
minted there. One leaked token never unlocks the whole account.

You can also **open a new workspace** for your human — the MCP tool
`create_workspace`, or:

    POST /api/agent/workspaces/new
    {"key": "td_…", "ws": "acme-docs"}   → {ok, workspace, token, git, web}

Same checks and caps as the /new page. The response's `token` is a fresh
credential minted **in the newborn workspace** — use it there; the one
you called with keeps acting only where it was minted. Your human sees
the new token on that workspace's /tokens page, revocable any time.

## Working over git

    git clone https://thousand:<token>@<host>/git/<workspace>.git

That is a complete, ordinary git repo — real paths, real history. Work
normally: pull, commit, push. What you must know:

- **The clone is your grant.** Folders you cannot see do not exist in the
  repo, not even in history. If a folder you expect is missing, you were not
  granted it — ask your human, do not go looking for it.
- **One commit per top-level folder.** Commit messages travel with shared
  folders, so a commit spanning two of them is refused at push. Split by
  folder, and write each message for the audience of that folder.
- **A rejected push explains itself.** Read the message; it names the file
  and the fix. Common ones: a path outside your write access, a commit mixing
  folders, an edit to a platform-owned file.
- **Platform-owned files.** Never edit `<folder>/.thousand-access.md`
  (generated audience notes) or `<folder>/.comments/` (comment sidecars),
  and never create `.thousand/` — access rules live on the server, not in
  files, and pushes carrying that directory are refused.
- Only `main` is served. History is append-only; force-pushes are refused.
- **Push through the host `workspace_info` hands you** (`git.usethousand.com`
  when hosted). It serves git directly, with no proxy in front, so a large
  first push carries the whole history in one go. The same path also answers
  on the main hostname, but a CDN proxy sits in front of it and cuts request
  bodies at 100 MB — a big push there dies with a bare `413` before Thousand
  ever sees it. Limits that are Thousand's own, with real error messages:
  25 MB per file, 1 GB per repo.

**The welcome folder.** A fresh workspace is born holding `welcome/` — a
few example documents showing what the place can hold. They are samples,
nothing depends on them, and everything fictional lives under that one
folder. If your human doesn't want them (importing a real repo, or just
done with them), remove them in one move — `git rm -r welcome` and
commit, or delete the folder in the app. Never treat their contents as
the team's real facts.

**Importing an existing repo.** A fresh workspace holds only the
`welcome/` examples above — a path no real repo owns, so nothing of
yours can collide with it — but its history is still unrelated to yours,
and force-pushes are refused, so the two are joined by a merge (drop the
examples in the same breath):

    git remote add thousand https://thousand:<token>@<host>/git/<workspace>.git
    git pull thousand main --no-rebase --allow-unrelated-histories
    git rm -r welcome && git commit -m "imported over the examples"
    git push thousand main

If the repo carries a `.thousand/` directory from an older Thousand
workspace, delete it first (`git rm -r .thousand` and commit) — the push is
refused while the current tree contains it, and rebuild the grants with
`grant_access` or on /access.

## Know your audience before you write

Every shared folder carries `.thousand-access.md` — who can see it, tier by
tier. Files AND commit messages there are visible to everyone it lists. Check
it before writing anything sensitive, or ask fresh for any path:

    GET /api/agent/audience?key=<KEY>&path=<path>

## Comments

Humans discuss documents in the margin; the discussion is stored as markdown
beside the document (`gtm/verdict.md` → `gtm/.comments/verdict.md`), so a
clone carries every conversation. Read them freely — `/api/agent/read`
returns a document's comments with it.

To leave one, never edit the sidecar — post it:

    POST /api/agent/comment
    {"key": "<KEY>", "path": "gtm/verdict.md", "body": "…",
     "quote": "<the passage it is about>",          # optional
     "parent": 0}                                   # optional: reply to id

It lands in the document's margin and in the sidecar, attributed to the user
your key maps to.

Clean up after yourself. Working notes you left in someone's margin are
yours to remove — and only this can remove them, since the sidecar is
platform-owned and git will not touch it:

    POST /api/agent/comment/delete
    {"key": "<KEY>", "id": 8}      # your own comments; owners, anyone's

Replies go with the comment. Deletion is not undoable.

## The link to hand your human

Every document has a web address, and it is not the file path alone:

    https://<host>/d/<path>          e.g. https://<host>/d/gtm/verdict.md

Every API answer that names a document carries the built URL in `url` (a
posted comment's `url` links straight to it), so quote that rather than
assembling one. Do not try to discover routes by probing: signed out, every
path answers the same redirect to /login, real or invented — deliberately,
so nobody can map a workspace from outside.

## The agent API

Base URL is the host you were given. Auth is the `key` parameter. You inherit
the ACL of the user the key maps to; sealed paths refuse.

Send a real User-Agent header naming your client (`my-agent/1.0`). A CDN
sits in front of the hosted API, and a default library UA like
`Python-urllib` can be refused at that layer with an opaque 403 error 1010
that never reaches Thousand.

**Read** — returns the file, its comments, current HEAD, and `base`
(a content hash — round-trip it when you edit):

    GET /api/agent/read?key=<KEY>&path=<path>

**Write** — you express edits; the platform owns git and decides how they land:

    POST /api/agent/edit
    {"key": "<KEY>",
     "task": "short-slug",            # optional, names the branch if isolated
     "message": "commit message",     # optional
     "edits": [{"path": "...", "text": "full new contents",
                "base": "<from read>"}]}   # optional but ALWAYS send it

`base` is your edit's seatbelt. An edit writes the whole file, and a human
may be typing in the same document right now — their draft is live in the
shared tree. With `base` set, a file that moved since your read refuses the
edit and hands you the current text and its new base: merge your change
into that text and resend. Without `base`, you overwrite them silently.

**Diff** — what changed, before you write or after you look away:

    GET /api/agent/diff?key=<KEY>&path=<path>            # uncommitted draft vs HEAD
    GET /api/agent/diff?key=<KEY>&path=<path>&since=<sha> # everything after that commit

Returns the unified diff, `dirty` (is there an uncommitted draft), `head`,
`base`, and with `since` the commit list too. An empty diff with
`dirty: false` means the document is exactly its last commit.

The response says what happened: `"mode": "live"` — in the shared tree,
visible immediately, auto-checkpointed when idle; `"mode": "isolated"` —
committed to a private branch, **invisible until a human merges it** on
/proposals. Policy picks the mode; you may request `"isolated"` but cannot
force `"live"`. The line is visible, not a trap: every response carries
`"gauge"` — this edit's `delta_bytes` (the size of the CHANGE, not the
file) and the `isolates_over` thresholds (defaults: 8000 changed bytes,
or more than 2 files; folders can be pinned either way by server policy).
An isolated response says `"why"`. Sending the same isolated edit twice
lands on the same branch — retries never stack duplicate proposals.

A live edit is not yet a commit. It is the same uncommitted draft a human's
typing makes — on screen for everyone at once, folded into an `[auto]`
checkpoint commit after ~20 idle minutes. This is the one write path with a
draft stage: a git push is a commit by definition.

**Audience** — see above. **Comment** — see above.

**Permissions** — what someone may do, folder by folder, without anyone
opening the /access page:

    GET /api/agent/permissions?key=<KEY>              # your own access
    GET /api/agent/permissions?key=<KEY>&who=sam    # owners only
    GET /api/agent/permissions?key=<KEY>&path=notes/**

**Access** — grant or revoke, per folder or per document:

    POST /api/agent/access
    {"key": "<KEY>", "who": "sam", "path": "collaborations/**",
     "tier": "commenter"}      # editor | commenter | reader | "" to revoke
    {"key": "<KEY>", "who": "sam", "path": "gtm/verdict.md",
     "tier": "reader"}         # a single document works too

Who may share follows the Drive rule: an **owner** governs everything; an
**editor** may share what they can edit, at up to their own tier; commenters
and readers share nothing. A single-document grant shadows the folder rule
for that document only; a folder-level change flows through every file rule
beneath it, so a folder revoke revokes everywhere. Any ancestor folder is a
grantable unit (`research/interviews/**`), and `"days": 30` makes a grant
temporary — it stops working the moment it expires. Rules live on the
server; every change is logged and live immediately, and the folder's
audience note is regenerated.

If a path you need answers "sealed", ask rather than guessing at rules:
your human can request access from the document page, and owners decide on
/access.

## Standalone agents

An agent can also be a member of its own — for shared bots and scheduled
jobs that do not act as any one person. An owner adds it on /access
("Add person, group, or agent"), grants it folders like anyone else, and
mints its token there. The token is the only way it signs in, and it
always expires (1–366 days). What its profile card shows is exactly what
the token can reach — check it before you paste the token anywhere.
Interactive assistants keep acting as the person who runs them; both
models coexist.

## Reconciliation

Owners can ask the janitor to read one folder ("tidy…" on the folder
page): a model finds duplicates, contradictions, stale pages and
orphans, and argues each one. Nothing is automatic — findings become a
proposal branch in the normal review flow, a human merges, and git keeps
every deletion reversible. Contradictions are only ever flagged; no
model picks which fact is true.

## Synced folders

A workspace can mirror a folder of another repository. The mirrored tree
is read-only here — the source repo stays authoritative, so edit there
and the change arrives on its next commit (the server checks each source
about once a minute). Every mirrored document carries a "synced from"
chip naming its source and commit. Access rules slice a mirror like any
other folder.

Connecting one is an **owner's** action, and you can do it for them —
the MCP tool `connect_source`, or:

```
POST /api/agent/sources
{"key": "td_…", "repo_url": "https://github.com/org/repo",
 "folder": "docs", "dest": "eng/repo-docs", "token": "…optional…"}
```

The first sync runs before the call returns, so a wrong URL, folder, or
token fails right there in the response. `GET /api/agent/sources?key=…`
lists what is connected (stored tokens never come back — only
`has_token`); `POST /api/agent/sources/sync` (`{key, id?}`) pulls now;
`POST /api/agent/sources/remove` (`{key, id}`) disconnects, and the
mirrored files leave with it.

**Private source repos** need a read-only credential that only the
hosting platform can mint — ask your human for it, with exact
instructions. On GitHub: Settings → Developer settings → Fine-grained
personal access tokens → scoped to that one repository, permission
Contents: read-only, with an expiry. On GitLab: a project deploy token
with `read_repository`. Thousand stores the token server-side, uses it
only to pull, and never echoes it back. Public repos need no token.

Only whitelisted extensions surface in the app (documents plus assets),
so point a source at a folder of context files, not at `src/` — code
would sync into the repo but stay invisible.

## The tiers

Per folder: **editor** (read + write), **commenter** (read + comment — the
comment endpoint is this tier's entire write surface), **reader** (read).
**Owner** is workspace-level and governs the rules, not the contents.

## The exit

Nothing is locked in. A full-access clone of the canonical repo is the whole
workspace — markdown files, comments included — usable without Thousand,
forever. If you are asked to migrate a team out, `git clone` is the entire
procedure.

---
Revision 1c956bba · 2026-09-04. This page changes when Thousand does; re-fetch (ETag `1c956bba`) before acting on a reading more than a few minutes old.
