zas-agent
An MCP server that lets a coding agent — Claude Code, Codex, or anything that speaks MCP — send files and notes into your Zas channels, and read items back out of them.
What is Zas
Zas moves things between your devices and the people you choose: files, photos, text, code. You put something into a channel on one device and use it on another. Whatever you do not pin leaves on its own after a few days, so there is nothing to tidy up later. Everything is encrypted on the device before it is uploaded: the database and the object storage receive encrypted bytes, not filenames, content or previews.
This package is the piece that lets a coding agent use your channels the way your other devices do, under an identity of its own that you approve, that you scope to the channels you choose, and that you can revoke.
What it does
Your coding agent gets eleven tools. It can send a file or a note into a channel you picked, send or receive a file live through Directo, list what is in that channel, and pull one item back onto disk. Everything it sends is encrypted on your machine before it leaves, lands in your account, and is marked in the channel as sent by that agent. The agent has an identity of its own and never holds your account key: pairing mints a key pair here, you approve it from the web app, and from then on it signs a challenge to get a short-lived session.
Install
Install Node.js LTS (22 or newer) and your agent's command-line tool first. Reopen your terminal after installing. Run these commands on the computer where you use the agent.
Choose your agent and pair it. On Windows (PowerShell or CMD):
Codex
npx.cmd -y zas-agent@latest pair --profile codex
Claude Code
npx.cmd -y zas-agent@latest pair --profile claude-code
Open the printed link, approve the channels, and enter the code in the terminal if the page shows one. Leave the terminal open until pairing finishes. Then run the matching registration command:
Codex
cmd /d /c codex mcp add zas "--" npx.cmd -y zas-agent@latest --profile codex
Claude Code
cmd /d /c claude mcp add zas "--" npx.cmd -y zas-agent@latest --profile claude-code
On macOS, Linux, or WSL, use npx in place of npx.cmd and omit
cmd /d /c . Pair and register in the same environment. Restart your agent
and ask it to send something to Zas.
If a command is not recognized, install Node.js or the named agent's CLI and
reopen the terminal. The first download needs internet access and may take a
few minutes. Use @latest as shown so npm requests the published package
instead of selecting an unbuilt local workspace.
What pairing does
zas-agent pair mints the key pair, registers the public halves, opens the
approval page in your browser, and waits. The terminal shows:
Open this page signed in to your Zas account:
https://zas.red/agents/pair?p=...#port=53211
Fingerprint: 1a2b 3c4d 5e6f 7a8b
Waiting for approval… (expires in 10 minutes)
Signed in, name the agent and tick the channels it may use — sending is the
default, reading is a separate switch — and approve. Approval creates
nothing by itself: the page hands a one-time claim code to this terminal
over 127.0.0.1 (the port in the link), and the agent exists only once the
terminal claims with that code and the secret it holds. A link that reached
somebody else is approved on their machine, where nothing listens, and
expires with nothing created.
If the browser cannot reach the terminal — the link was opened on a phone,
or the browser refused the local connection — the page shows the code and
the terminal asks for it. Type it there and nowhere else: with that code,
another terminal that started a pairing could claim it. Pass --no-open or
set ZAS_NO_OPEN=1 to keep the browser closed; the link is printed either
way. The pairing is good for ten minutes before approval and five after;
past that, run the command again.
You can also start the flow from the coding agent with the zas_pair tool:
the first call hands back the URL, a later call says whether the approval
landed, and if the page shows a code, a call with code claims with it.
Why you can trust it
Every line here is a fact. The ones about this package you can check in this repository. Where a property is enforced by the Zas server, which is not open source, the sentence says so.
- The agent has an identity of its own.
zas-agent pairgenerates two key pairs on your machine — X25519 to receive channel keys, P-256 to sign sign-ins — and the private halves never leave it. The agent never holds your account key, and the key-derivation service refuses account-key derivation to an agent (server-side). - You approve it, and you choose the channels. Pairing never auto-approves. The approval page shows the harness, the host and the key fingerprint, and nothing exists until the terminal that started the pairing claims it with a code only the approving page received (server-side). A pairing link sent to you by someone else creates nothing on your account.
- It holds one key per granted channel, and no key for any other. Each grant carries that channel's key sealed to the agent's X25519 public key. A channel you did not grant has no key here to decrypt with, and the server checks the live grant on every request (server-side).
- No password and no API key. Signing in is a signed challenge traded for a one-hour token. There is no password, no API key and no refresh token on disk: the agent re-signs from its P-256 key when the token ages out.
- Everything it sends is visible as its work. Every item carries the
>_agent mark and the agent's name, in the channel, on every device you read Zas from. - You can revoke it at any time. Settings → Agents → the agent → Revoke. The session stops, its refresh tokens are revoked (server-side), and the next tool call answers "the owner revoked this agent". What it already sent stays where it is. You can also drop a single channel and keep the rest.
- Content is encrypted on your machine before it leaves, the same way the
app does it. The
chunking, the manifest and the envelope formats under
src/shared/are the same modules the Zas web app ships. The server stores ciphertext and never sees a channel key, a channel name or item plaintext (server-side). - The source is here, and the releases are built from it. Every npm release
is published by the
release.ymlworkflow in this repository, with npm provenance, so the tarball on npm can be traced back to a commit and a workflow run.
What it cannot do
The package refuses some of these on its own, before a request is made. The ones marked (server-side) are enforced by the Zas server.
- No channel you did not grant. Not by name, not by id.
- No shared channel you merely joined, and no workspace channel. Grants exist only on channels your account owns and that no organization manages (server-side).
- No reading unless the grant says so.
readis a separate switch fromsend; without it,zas_list_itemsandzas_get_itemare refused. - No stored sending into a view-only channel, and none into a channel in
Directo mode. Both are refused before a byte is uploaded. A channel in
Directo mode takes
zas_send_direct, a live transfer that stores nothing. - No receiving unless the grant says read. Receiving through Directo
writes a file onto this machine, so
zas_receive_directtakes the samereadswitch as listing. And it only ever runs inside a tool call: the agent never watches your channels, so nothing arrives unasked. - Nothing outside its allowlist. The API refuses an agent on every route that is not on a short, explicit list, and Firestore rules refuse it your account document, your devices, and any channel without an active read grant (server-side).
- Rate limited by the server, on its own buckets, with the key-derivation budget charged to your account so ten agents are not ten times your own allowance (server-side).
- As many agents as your plan or your organization allows (server-side).
- Files up to 5 GiB, and in practice less: the agent reads a file into memory to hash it, so the machine's memory is the real ceiling.
The error vocabulary
The agent answers in a closed set of codes. Anything a server route says that
is not in this set collapses to upload_failed or network, so no raw server
string ever reaches a terminal.
| Code | What it means |
|---|---|
not_paired | This agent is not paired yet. |
identity_corrupt | The identity file on disk is damaged. |
agent_revoked | The owner revoked this agent. |
agent_forbidden | Only the account owner can do that. |
grant_missing | This agent has no access to that channel. |
send_forbidden | This agent cannot send to that channel. |
read_forbidden | This agent cannot read that channel. |
direct_mode | That channel is in Directo mode. |
not_direct_mode | That channel is not in Directo mode. |
not_claimed | Nobody received the file within ten minutes; the offer was withdrawn. |
no_offer | Nobody offered a file through Directo while the call waited. |
offer_taken | Another device received that file first. |
direct_cancelled | The offer was cancelled from the other side. |
direct_failed | The Directo transfer failed in flight. |
direct_not_failed | That job is not a Directo transfer that failed in flight. |
file_changed | The file changed since the Directo offer. |
webrtc_unavailable | The WebRTC engine could not be loaded on this machine. |
fallback_unavailable | Reliable delivery is not available right now. |
key_stale | The channel key changed; the owner refreshes it by opening Zas. |
quota_exceeded | The account reached its storage limit. |
rate_limited | Too many sends in a row. |
file_too_big | The file is over the plan limit. |
duplicate | That item is already in the channel. |
not_found | That item is not in the channel. |
invalid_cap | That file is no longer available. |
write_failed | The download destination could not be written. |
pairing_expired | The pairing expired; pair again. |
pairing_cancelled | The owner cancelled the pairing. |
feature_disabled | Agents are not enabled for this account yet. |
upload_failed | The upload failed. |
oprf_failed | Zas did not answer correctly while preparing the file. |
network | Zas cannot be reached. |
sign_in_failed | Zas did not accept this agent session. |
bad_signature | Zas rejected this agent's signature; pair it again. |
missing_token | The session token is missing; pair the agent again. |
internal | Something failed inside the agent. |
Every code comes back as one sentence in English, never as a stack trace.
Tools
| Tool | What it does |
|---|---|
zas_status | Says whether this agent is paired with a Zas account, and lists the owner's channels it may send to or read from. |
zas_pair | Pairs this agent with a Zas account. The first call returns a URL for the owner to open; a later call says whether they approved. If the page shows a code, a call with code claims with it. In a profile that is already paired, approval replaces the old agent. |
zas_send_file | Sends a file from this machine into one of the owner's channels. Returns the item id, or a job id when the upload takes longer than a minute. |
zas_send_note | Sends a note — plain text, or a code snippet with its language — into one of the owner's channels. |
zas_send_direct | Sends a file through Directo: a live, device-to-device transfer into a channel in Directo mode. Nothing is stored. The owner presses Receive on another device within ten minutes; the call returns the result, or a job id after a minute. |
zas_send_direct_fallback | After a Directo send failed in flight, delivers the same file through reliable delivery: encrypted on this machine, stored in Cloudflare R2 for up to 24 hours, off the owner's quota. The owner's choice; the model is told to ask. |
zas_receive_direct | Receives a file the owner sends through Directo, onto this machine. Waits for the offer, takes it, and writes the file to disk. Needs a grant that includes reading, and a channel in Directo mode. Returns the path written, or a job id after a minute. |
zas_receive_direct_fallback | After a Directo receive failed in flight, downloads the encrypted copy the sender chose to store, and decrypts it to the same destination. |
zas_list_items | Lists the most recent items in one of the owner's channels. Needs a grant that includes reading. |
zas_get_item | Fetches one item. A note comes back as text; a file is written to disk. It never overwrites, so the path it answers with can differ from the one you asked for. |
zas_jobs | Lists the sends and Directo transfers this server started, newest first, with the phase each one reached — and where a job_id from a long send is redeemed. |
channel takes a channel name or a channel id. A name has to match exactly one
of the channels you granted; with exactly one grant, zas_send_file,
zas_send_note, zas_send_direct and zas_receive_direct can leave it out.
Directo needs a native module, node-datachannel,
WebRTC for Node. npm installs a prebuilt binary for Windows, macOS and Linux;
the module loads the first time a Directo tool runs, and a machine where it
cannot load answers webrtc_unavailable. Every other tool works without it.
Two things worth knowing before you point a model at your account:
zas_send_fileandzas_send_directsend any file this process can read —~/.ssh/id_rsaand a.envincluded. Confirm with the owner before sending secrets, keys or credentials. Its tool description says so, so the model reads it too.zas_get_itemandzas_receive_directwrite a new file underdest, or under the system temp directory when you leavedestout. Neither ever overwrites an existing file: a name that is taken gets a suffix, and the path they answer with is the one they actually wrote.
Everything those tools touch lands inside your own account and your own machine. Revoking the agent stops all of them.
What it sends back
This package reports how it is used, so defects like a release that could not receive a file at all stop being invisible. It is on by default and it is yours to turn off:
npx -y zas-agent telemetry off # and `on` again, and `telemetry` to see
ZAS_AGENT_TELEMETRY=0 and the cross-vendor DO_NOT_TRACK=1 do the same
without writing anything. The choice lives in ~/.zas/agent/settings.json and
covers every profile on the machine; pairing again does not reset it.
zas_status always says which way it is set.
What one report carries, and nothing else: the tool that ran, whether it worked, the closed error code when it did not, which of four duration buckets it fell in, and the package version. No file name, no file contents, no path, no channel name, no error message, no stack.
Where it goes: to Zas, never to an analytics service directly. This package holds no analytics token and opens no connection to a third party, so no address of yours reaches one. The report is attributed to the Zas account the agent is paired with, which the server reads from the session — the request carries no identifier of its own. Nothing is reported before pairing, because until then there is no account it could belong to.
A report is never allowed to matter: it is sent after the answer, waited on for at most five seconds, tried once, and dropped in silence if it fails.
Data on disk
One directory per profile, so one machine can hold a Claude Code agent and a Codex agent side by side without either reading the other's keys:
| OS | Path |
|---|---|
| macOS, Linux | ~/.zas/agent/<profile>/ |
| Windows | %USERPROFILE%\.zas\agent\<profile>\ |
Four files in the profile directory, all written through a temporary file and renamed into place, so a crash mid-write cannot leave half a file behind:
identity.json— the agent uid, the owner uid, the name, and the two key pairs. Back it up like a private key, or delete it and pair again.pending.json— a pairing that has not been claimed yet. Removed on completion, and on a pairing that expired or was cancelled.grants.json— a one-minute cache ofGET /v1/agents/me: which channels, and the sealed key for each. The channel name stays encrypted here. Disposable.fingerprints.json— hashes of what an identical send produced in the last ten minutes, so a retried tool call answers without touching the network. It stores hashes, never a title or a note's first line. Disposable.
One more file sits next to the profiles, in ~/.zas/agent/:
settings.json— whether this machine reports usage, and when the notice above was last printed. It belongs to the machine, not to a profile, so one decision covers every agent on it.
On macOS and Linux the directory is created 0700 and every file 0600. On
Windows those bits have no effect: the files carry the permissions of the user
profile they live in, and the package does not try to set any others.
Deleting the directory makes this machine forget the agent. It does not revoke anything: the account side is closed from the web app, under Settings → Agents → Revoke.
Pairing again
A pairing is one profile's key pair, not the machine and not the program. A
profile holds one agent. Running zas-agent pair in a profile that is already
paired opens a replacement: the terminal signs the request with the old
identity, the approval page says which agent it replaces and fills in its name
and channels, and the old agent is revoked in the same step that creates the
new one. Its row stays under Settings → Agents as revoked until you remove it,
and it keeps costing a slot until then. If the old agent was already revoked
or removed, pair says so and creates a new agent.
One machine that runs Claude Code and Codex has two profiles (claude-code
and codex), two agents, two approvals and two rows. Revoking one does not
touch the other. Two programs pointed at the same --profile act as one
agent: same keys, same channels, one row.
Configuration
| Setting | Default | What it changes |
|---|---|---|
--profile <name> | claude-code | Which identity directory this process uses. Letters, digits, ., _ and -, up to 64, and it may not start with a dot. |
ZAS_AGENT_HOME | ~/.zas/agent | Where the profile directories live. |
ZAS_AGENT_TELEMETRY | unset | 0 turns usage reporting off for this process, 1 on. It outranks the stored choice. |
DO_NOT_TRACK | unset | 1 turns usage reporting off. Read only to turn it off. |
ZAS_WEB_BASE | https://zas.red | The web app the pairing URL points at. |
ZAS_API_BASE | https://zas.red/api | The API. |
ZAS_TOKEN_BASE | https://zas.red/anon-token | The challenge and token routes. |
ZAS_OPRF_BASE | https://zas.red/oprf | The blind key-derivation service. |
ZAS_FIREBASE_PROJECT | zas-me | The project whose Firestore the read path queries. |
ZAS_FIREBASE_API_KEY | the public web key | The key used to exchange a custom token for a session. |
Only --profile, ZAS_AGENT_HOME and the two telemetry switches are worth
setting by hand. The rest exist
so the package can be pointed at a test deployment.
Development
git clone https://github.com/soke1556/zas-agent
cd zas-agent
npm ci
npm test
npm run typecheck
npm run build
node dist/cli.js --version
This repository is the public mirror of the agent/ package of Zas's private
monorepo. It is produced by scripts/export-public.mjs, which builds the
package with esbuild and uses the build's own metafile to decide what to copy:
src/, test/, the package files, and the nine src/shared/* modules the
agent imports. Those shared modules are the client-side crypto and format code — the
chunker, the manifest, the envelope, the key derivation — that the Zas web app
also ships, which is why the encryption the agent performs is the encryption the
app performs. The Zas server is not open source.
Pull requests are welcome against this repository. The maintainer applies accepted changes back into the monorepo, and the next export brings them here. See CONTRIBUTING.md.
Security
Please report vulnerabilities privately. SECURITY.md says how, and what to expect.
License
MIT. See LICENSE.