
Security News
PHP Foundation Says Chainguard Broke Security Embargo Ahead of Emergency Patches
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.
@mattstack/rt-client
Advanced tools
Typed client for the rt daemon: repos, worktrees, ports, tokens, and the event relay
Typed client for the rt daemon.
rt keeps the per-repo state a dev environment needs: worktrees, assigned ports, tokens, dev servers, and a live event relay. This package is how other programs read and drive that state without shelling out to the CLI and parsing text.
bun add @mattstack/rt-client
import { rtCommand, readProjectMRs } from '@mattstack/rt-client';
const repos = await rtCommand(['repos', 'list']);
const mrs = await readProjectMRs('group/repo');
Requires a running rt daemon. Install rt from the latest GitHub Release
(./rt --post-install), then rt verify.
Bun-only: the settings exec path (src/settings/exec.ts) shells out via
Bun.spawn, so this package does not run under Node.
@mattstack/glance is a peer dependency: rt-client returns glance's forge types
so merge request shapes stay identical across rt, gitq, and mr-board.
Every per-repo key in the rt estate is a stable serialized identity, not a
repo name. Whatever you store per-repo, send to the daemon, or put in a REST
path is keyed by the wire form this package emits. The one exception:
settings-store sections (repos.<identity>) key on the RAW host/path form
(RepoIdentity.id for a remote-kind identity) — the settings resolver never
sees the wire form, and a serialized key there misses silently.
remote:gitlab.com%2Facme%2Facme-dev path:%2FUsers%2Fdev%2Fscratch
└─┬──┘ └──────────┬─────────────┘
kind the id, encodeURIComponent'd — slash-free, fits one URL segment
kind is remote (the repo has an origin: id is normalized host/path) or
path (no usable remote: id is the main worktree's realpath). The : is a
literal delimiter.
import {
deriveRepoIdentity, // (repoPath: string) => Promise<RepoIdentity> — never null
serializeIdentity, // (id: RepoIdentity) => string — the wire form above
parseIdentity, // (wire: string) => RepoIdentity | null — THE validity check
identityFromRemote, // (remoteUrl: string) => RepoIdentity | null — sync
type RepoIdentity, // { kind: "remote" | "path"; id: string }
} from '@mattstack/rt-client';
const identity = serializeIdentity(await deriveRepoIdentity(repoPath));
await rtCommand(['worktree', 'list', '--repo', identity]); // daemon key
const url = `/api/runs/${encodeURIComponent(identity)}/${runId}`; // URL segment
| Do | Don't |
|---|---|
| Get identities from these functions, once, at the boundary | Re-derive with your own git calls (git remote get-url diverges under insteadOf) |
| Key stores and daemon payloads on the serialized form | Key anything on a folder basename or a remote's last segment |
Key settings sections (repos.<identity>) on the raw host/path id | Put the serialized form in a settings lookup, or the raw form in a daemon payload |
encodeURIComponent(identity) in URL path segments | Ship the wire form raw in a URL — its % signs decode into slashes |
Decode for display: parseIdentity(wire), then the id's last path segment (remote) or basename (path) — the returned id is already decoded | decodeURIComponent the id again, show the wire form to a human, or build a chat handle from it |
Treat the repo field from runs:list as an opaque key, passed back verbatim | Validate or re-derive runs:* repo keys — pre-cutover runs keep their original keys |
Repo-keyed daemon verbs accept serialized identities only; a bare repo name
doesn't error, it resolves empty. parseIdentity is strict — only strings
serializeIdentity emitted parse — so validate payloads with it, and never
hand-assemble or string-split a wire.
The rt chat surface the chat viewer (~/Documents/GitHub/chat) is built
on. Every call degrades to { ok: false, error } instead of throwing, and a
daemon that is down and a daemon that refused look the same to the caller;
branch on ok, not on exceptions.
import { chatRooms, chatMessages, chatPost, createRelay, daemonHealth } from '@mattstack/rt-client';
const rooms = await chatRooms({ handle: 'matt' }); // { ok, data: { rooms } }
const page = await chatMessages({ room: 'build', limit: 50 }); // newest page; `before: <id>` pages older
await chatPost({ handle: 'matt', room: 'build', body: 'on it' });
const health = await daemonHealth(); // { reachable, error? }, never throws
const stop = createRelay({ // one daemon subscription, republished
match: t => t.startsWith('chat/'), // `chat/<room>/msg`
topic: 'chat',
publish: (topic, data) => server.publish(topic, data),
});
| Function | Daemon verb |
|---|---|
chatSignIn / chatSignOut / chatAway / chatBack | presence: the buddy-list row and its session heartbeat |
chatBuddies / chatWho / chatRooms | the roster, one room's members, a handle's rooms |
chatJoin / chatLeave / chatArchive | membership (wakeOn: mention | all | none); archive parks a room for everyone until a post revives it |
chatPost / chatDm / chatDmOpen / chatRead / chatMessages / chatMark | messages: post, DM, open a DM room without posting, read-and-advance, page, advance the cursor |
paneList / panePeek / paneSpawn / paneAccounts / paneDirectories | herdr panes: list with presence joined, peek a screen, start claude in a tab, cswap accounts, directory suggestions |
chatInvite | type /chat:join <room> into a pane; accepted, queued or refused |
paneSend | type a line into a pane; accepted, queued or refused, plus an optional continuation the daemon types once the pane's turn has ended |
createRelay / subscribe | the event stream; daemonHealth the reachability probe |
Pass { sockPath } as the trailing options to reach a non-default daemon
socket. The verbs and their payloads are specified in repo-tools
docs/superpowers/specs/2026-08-28-rt-chat-delivery-v2-design.md; the
agent-facing rules are skills/rt-chat/SKILL.md.
Every object or array settings key has a JSON Schema describing the value
its readers accept. The schemas are authored in zod
(src/settings/registry-schemas.ts) and committed as
src/settings/schema.lock.json; the registry attaches each def's schema
(and, for a deep-merged object, its layerSchema) from that lock, so zod
never loads at runtime. Checks run through @cfworker/json-schema.
import { getDef, checkSchema, validateWrite, checkStores } from '@mattstack/rt-client';
const def = getDef('rt.notify.eventBridges')!;
checkSchema(def, [{ pattern: 1 }], { layer: false }); // [{ path: [0, 'pattern'], message: 'expected string, got number' }]
const verdict = validateWrite(def, [{ pattern: 1 }], { scope: 'user' }); // also takes repoIdentity, team
if (!verdict.ok) console.error(verdict.reason); // "[0].pattern: expected string, got number"
const { findings, failing } = checkStores(); // what `rt settings check` prints
| Function | What it does |
|---|---|
checkSchema(def, value, { layer }) | every issue against the layer schema (layer: true) or the full schema; [] for a key with no schema |
validateJson(schema, value) | the same check against a bare JSON Schema |
validateWrite(def, value, { scope, repoIdentity?, team? }) | the write gate setSetting runs: type and path guard, the layer, then the merged result, refused only when the write makes a passing merge fail |
checkStores() | every stored value, every merged value and every unregistered key; failing excludes unregistered keys; a merged finding has no scope or file |
listUnregisteredSettings() / repoSectionsFor(key) / listStoreRepoIdentities() | unregistered keys in stores, which repos set a key in which stores, every identity with a repos.<id> section |
mergedValueWith(def, { scope, value, repoIdentity?, team? }, opts) | the value the resolver would merge with that one layer replaced |
currentMergedValue(def, opts) | the value getSetting would return, without its warnings for skipped layers |
Reads never skip a value that fails only its schema: explainSetting rows
carry nonconforming issues and listSettings carries nonconforming and
mergedIssues, while the value stays in effect. The lock tooling
(buildLock, classifyLockDiff) is not on this entry point; rt's
rt settings schema lock and rt settings schema diff use it from source.
RunStageRow.status is one of running | done | failed | redirected; rt runs stage-redirect writes the fourth when the work engine leaves a stage for another one, so a reader that maps statuses to icons or filters must handle all four.
MIT
FAQs
Typed client for the rt daemon: repos, worktrees, ports, tokens, and the event relay
The npm package @mattstack/rt-client receives a total of 0 weekly downloads. As such, @mattstack/rt-client popularity was classified as not popular.
We found that @mattstack/rt-client demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.

Security News
PHP releases emergency patches for two critical RCE vulnerabilities as agencies urge organizations to take unpatched sites offline.

Threat Research
/Security News
A supply chain attack compromised LiteLLM 1.107.0 on PyPI and Docker, spreading malware that steals cloud credentials, developer secrets, and AI provider keys.