PostMQ in Claude Code.
One .mcp.json entry connects Claude Code to the MCP server; one restart makes the tools callable. The rest — a session that resumes where the last one stopped, decisions logged as they are made, usage reported when the session ends — is a set of skills and hooks this repository runs on itself, described here closely enough to copy.
one session in Claude Code
.mcp.json → postmq · type httpstart_build_session → 200 · resumed · project · computer · branch · actor
append_decision_log → 201 · linked to the open session
end_build_session → 200 · history entry persisted
SessionEnd hook → record_build_session_usage · token counts, not dollars
the calls a session makes at its bracket ends · illustrative
An MCP server Claude Code calls, and a protocol we run on it
PostMQ speaks standard MCP, so any MCP-capable client can connect — the
MCP clients page carries the full list; Claude Code is the client we run
ourselves every day, alongside Codex — including
the hand-offs of the multi-agent code review loop,
where one agent builds and the other reviews from its own inbox. It sees the same 87 tools every client sees — over Streamable HTTP at
https://mcp.postmq.com/, or over stdio for a co-located deployment — with a scoped, once-disclosed
credential as the bearer on every call. The session protocol on top of it — start or resume a session,
ask which rules apply, work, log each decision, file each deferral, end with a history entry, report
what it consumed — is the nine steps on the session-state
page, and here it is the concrete form: two Claude Code skills, a SessionStart hook and a SessionEnd
hook. That part is how we run it, a recipe you copy; the server does not enforce it. The commit that
ships the work carries the session id as a trailer, so a reader can go from a commit to its session and
to the decisions it logged. For why a session needs any of
this — what context loss is between sessions, and where the hand-rolled fixes break — read
the context-loss guide; for the two
skills step by step as we run them, with the numbers, read
the start-session and end-session guide.
Five steps, two of them optional
Sign in, add one entry, connect, restart. The client either approves over OAuth or carries a credential you issue; the skills step is the protocol.
-
Create a workspace
Sign up at app.postmq.com. A workspace belongs to the person who creates it. Where the client connects over OAuth you do not create an AI account or a credential up front — approving the connection issues the credential, and creates the AI account too if the workspace has none yet. Approving is also yours to do.
app.postmq.com/signup -
Add the postmq entry to .mcp.json
Streamable HTTP at mcp.postmq.com. PostMQ refuses the first call with a 401 naming its OAuth metadata, so a client that implements MCP authorization registers itself and opens a browser for you to approve — no credential in the file. Where the client does not, add the Authorization: Bearer header from the mcp_config block instead. Project scope (.mcp.json at the repository root) is what we use, so every session in that checkout sees the server.
type: http -
Or, with no browser, an issued credential
Approval happens in a browser, so a headless box uses a credential issued in the mcp_config form instead — as an Authorization header, or over stdio with pmq mcp stdio reading POSTMQ_CREDENTIAL. stdio changes the wire, not where the records live; it still needs a connection to the hosted database.
pmq mcp stdio -
Restart Claude Code once
Claude Code fetches each server’s tool catalog when it starts. A client that was already running answers "tool not found" for a tool it has not seen; one restart refetches the catalog and every tool is callable.
restart once -
Install the skills and the two hooks
The /start-session and /end-session skills, the SessionStart hook that prints the drift signals, and the SessionEnd usage hook. They are ours and live in our repository, which is private — but their content is not: every one is served at postmq.com/kit/, and the plugin, an agent, or a copy by hand all take those same bytes. The section below describes each closely enough to reproduce.
.claude/skills · .claude/hooks
The restart is the step people skip. Claude Code fetches each server’s tool catalog when it starts, so a client that was already running answers "tool not found" for a tool it has not seen; restart Claude Code once and the catalog is refetched. That is the client’s behaviour as we have measured it in our own sessions, not something the server controls.
The .mcp.json entry
A client that implements MCP authorization needs only type: http and the URL here — it asks
for its own credential and you approve it once, so no secret goes in the file. The entry below is the
other form, for a client that does not or a machine with no browser to approve in: issue the credential
in the mcp_config form and the reveal prints the mcp_config
block first, shown once, with a copy button — name, transport, the URL, the Authorization: Bearer
header — and the stdio block under it. The postmq entry below is that block field for field:
type: http, the URL, the header. It is the same snippet the
docs page and the MCP server page show, rendered from
one source, so the three cannot disagree. Only the secret is redacted.
{
"mcpServers": {
"postmq": {
"type": "http",
"url": "https://mcp.postmq.com/",
"headers": { "Authorization": "Bearer pmq_…redacted…" }
}
}
} {
"mcpServers": {
"postmq": {
"type": "stdio",
"command": "pmq",
"args": ["mcp", "stdio"],
"env": { "POSTMQ_CREDENTIAL": "pmq_…redacted…" }
}
}
} The stdio alternative
pmq mcp stdio runs the same server over stdio for a self-hosted or co-located deployment. The
MCP wire has no Authorization header there, so the credential is process-ambient: the host reads
POSTMQ_CREDENTIAL first — the key the paste-ready block sets — then PMQ_CREDENTIAL, the
earlier name kept so an existing local setup keeps working, then the ~/.pmq/credentials file; it
re-reads them on every request, so a rotated value is picked up mid-session, and with none of the three the
request fails closed as unauthenticated. The pmq CLI is built from source; packaged downloads are
not yet published.
How we run it: two skills, two hooks
This is the part the server does not enforce. It is the recipe this repository runs at every session boundary, and each row names the PostMQ tools it calls.
/start-session has 5 steps: read the project’s law files
(the conventions that override default behaviour), query the current state, surface drift, pick a worktree and
a branch off origin/main, and resume or mint the session — resumed if this project, computer,
branch and actor already have an active one, enforced by a unique index. /end-session has
7 steps: log every material decision, file every deferral, refresh the
project’s status file and regenerate its launch plan (both ours), merge origin/main in as a
pre-merge guard, write the PR body, verify CI on every PR the session touched, close the session with a
history entry, and clean the worktree up. Neither skill is a request — both run at every session boundary,
and skipping one is itself a decision to be flagged.
You do not have to copy these by hand. They ship as the
session-state kit — both skills, both hooks and the settings
to wire them — installable as a Claude Code plugin with
claude plugin marketplace add No-Compromise-AI/postmq-plugins, or by
copying the files from postmq.com/kit/. An agent that
has just connected the MCP server will offer to install it for you, and will ask first.
pmq kit install --all writes the same tree in one command, but the CLI
is not a download today — it ships in no package manager and has no release workflow, so it is reachable
only by building it from source.
The table below is the protocol as this repository runs it against its own session state, with the step numbers from the copies we run. The kit you install is the same protocol against PostMQ, and it carries one instruction this repository's copies do not: record each pull request at the moment you open it, rather than only at the close, because a session that crashes or runs out of context never reaches its close at all.
| where | step | PostMQ tools | what it records |
|---|---|---|---|
| /start-session | 2. Query for current state | list_build_sessions, list_decision_log, count_backlog_items_by_status | the active session (the resume target), the last ten decisions, the open and resolved counts |
| /start-session | 5. Resume or mint the session | start_build_session, ping_build_session | resume the active session for this project, computer, branch and actor and ping it; otherwise start one and record its id in a state file the usage hook reads |
| /end-session | 1. Log decisions | append_decision_log | one entry per material decision; no silent decisions |
| /end-session | 2. File deferrals | file_backlog_item, triage_backlog_item | every deferred item gets a row; the items you finished are resolved with closing notes or a closing PR |
| /end-session | 6. Close the session | end_build_session | the closing history entry, the PR URLs and commits the session produced |
| SessionEnd hook | SessionEnd | record_build_session_usage | the transcript’s token usage against the session, self-reported by the hook |
The SessionStart hook is a shell script wired in .claude/settings.json. It prints
the protocol reminder, origin/main’s last commit and its age, the active worktrees, and every open
PR with its CI rollup and merge state — read from the checkout and gh; it fails open, so no
network prints nothing rather than an error. The SessionEnd hook is
postmq-usage-reporter: one Python file with no dependencies that reads the just-finished transcript,
sums five token classes, turns and sidechain turns, and records them with
record_build_session_usage
(or POST /v1/usage; one function builds one payload for both) against the session named by the
state file /start-session wrote. The numbers are self-reported by the hook — token counts, not
dollars — and idempotent per transcript. Any other client can call the same tool with the same arguments.
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start-reminder.sh\"" } ] }
]
}
} {
"hooks": {
"SessionEnd": [
{ "hooks": [ { "type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/postmq-usage-reporter.py\"" } ] }
]
}
} All 87, by domain — the protocol calls 10
Generated at build from the server’s own manifest and the same owner mapping the catalogue uses. Every name links to its row; the last column is the subset the skills and the hook call.
What the hook prints, and the three calls at the bracket ends
The first panel is the shape of the SessionStart hook’s output — its section labels and line formats are the script’s, the reminder lines are abridged, and every value is a placeholder. The other three are the calls, with real argument names and placeholder values.
== PostMQ session protocol == - Run /start-session before any material work; /end-session before you stop. - Work in a worktree on a branch off origin/main; one concern per PR; squash + auto-merge. - Session state lives in the MCP server (build sessions + decision log + backlog), not markdown. - A push exiting 0 != shipped: verify CI green AND the merge contains your latest commit. - origin/main last commit: <age, e.g. 2 hours ago> (<sha>) == active worktrees == <path>/.claude/worktrees/<name> <sha> [<branch>] == open PRs == #<n> [ok] merge=<CLEAN | BLOCKED | DIRTY> (<title>) #<n> [FAIL x1] merge=<state> (<title>)
{
"method": "tools/call",
"params": {
"name": "start_build_session",
"arguments": {
"project_id": "01K…",
"computer": "my-laptop",
"branch": "feat/lease-sweep",
"intent": "make the expiry sweep lose to a live lease",
"client_name": "claude-code"
}
}
} {
"method": "tools/call",
"params": {
"name": "append_decision_log",
"arguments": {
"title": "Ack and sweep predicates live in one UPDATE",
"body_markdown": "Both predicates in the same statement; the loser matches zero rows.",
"entry_date": "2026-08-18",
"has_public_contract_impact": false
}
}
} {
"method": "tools/call",
"params": {
"name": "end_build_session",
"arguments": {
"build_session_id": "01K…",
"session_history_entry_markdown": "Lease sweep loses to a live lease; ack refuses a re-pended row. Next: the crossing-boundary case.",
"related_prs": [{ "url": "https://github.com/…/pull/521" }],
"related_commit_shas": ["39b8680"]
}
}
}
The start returns 200 when this project, computer, branch and actor already have an active
session and 201 when it mints one — a unique index makes that true, so a reconnect never
duplicates a session. Each append_decision_log made while the session is open links to it without
being told to; the log is append-only by a database trigger. The
end persists the history entry the next session reads first, with the PR URLs and commits. Then the work
commit carries the session id as a trailer, and the SessionEnd hook records the transcript’s tokens against
the same session — the numbers below are this repository’s own, generated at build from its ledger and git
history and never typed; the dogfood check fails the build when the file goes stale.
As of 2026-09-02 — the project's own ledger and git history, generated at build; the dogfood check fails the build when the file goes stale.
6 things that go wrong, and why
Each row names the mechanism. The last one is our repository’s CI, not the product, and says so.
| symptom | why | what to do |
|---|---|---|
| A tool answers "tool not found" | Claude Code fetched the server’s tool catalog when it started; a tool that shipped after that is not in the client’s copy. | Restart Claude Code once. The catalog is refetched at startup and the tool becomes callable — the client’s behaviour, not the server’s. |
| Every call fails as unauthenticated | There is no anonymous path: a missing header, an expired or revoked credential, or a revoked parent account all fail closed at the tool’s scope check with a coarse error, and the plaintext is once-disclosed — the server cannot show it again. | If the agent holds its own OAuth token, check Connections in the dashboard — a disconnected grant revokes every token behind it, and reconnecting is one approval. If a person issued the credential, rotate or reissue it in the mcp_config form and replace the block. A tool outside the credential’s scopes fails the same way, so check the scopes it was granted. |
| stdio connects but calls are unauthenticated | pmq mcp stdio reads the credential from POSTMQ_CREDENTIAL first, then PMQ_CREDENTIAL, then the ~/.pmq/credentials file — fresh on every request, so a rotated value is picked up mid-session; a value that already carries the Bearer prefix is accepted. | Set POSTMQ_CREDENTIAL in the entry’s env block (the stdio snippet above sets exactly that key), and make sure the pmq process can reach the hosted database. |
| The SessionStart hook shows an old origin/main, a stray worktree or a failing PR | The hook prints origin/main’s last commit and its age, the active worktrees, and every open PR with its CI rollup and merge state; it fails open, so a missing gh or no network prints nothing rather than an error. Those are the drift signals — it does not read the ledger itself. | The start-session skill’s rule: if the last commit, decision or session is more than about 24 hours old and you are picking up material work, list the gap before doing new work — resume the recoverable worktree, update the blocked PR, then start. |
| The usage hook says "not attributed" | The reporter finds the build session through the state file /start-session writes (.claude/state/active-build-session-<claude_session_id>) or POSTMQ_BUILD_SESSION_ID; with neither it never guesses. It also needs POSTMQ_CREDENTIAL with the write_session_state scope. Every failure is one stderr line and exit 0 — a hook must never block session end. | Run /start-session so the pointer exists, export the credential the mcp_config block carries, and re-run: recording is idempotent per transcript, so a late report overwrites and never double-counts. On Windows use py -3 in the hook command. |
| The pull request’s discipline gate is red in our repo | In our repository a discipline gate greps the PR body for three literal predicates: a line naming the session id with the colon, a Decisions heading, and a Follow-ups heading (or a word such as backlog or deferred). It reads the body, not the commit message, and it re-runs when the body is edited. | Add the three lines to the body and save it — the check re-runs on edit. It is a required check in our repository, so a miss blocks the merge. This is our CI’s rule, not the product’s — the server records the session whether or not a PR body mentions it. |
Last verified 2026-09-03 against main at 2ececc05: the two skills,
the two hooks, the stdio credential source, the credential reveal and the config module this page names were
read on that day.
Related: session state — the protocol these skills run · the MCP server and its tool catalogue · connect an agent · the decision log a session writes to · credentials and the audit chain.
Add one entry. Connect. Start a session.
Add the postmq entry to .mcp.json, connect it, and call start_build_session from Claude Code.