Odel
Zas

Zas

Local
@soke1556TypeScriptMITUpdated Yesterday

Move files and notes between a coding agent and your Zas channels, encrypted here or via Directo.

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.

npm CI CodeQL OpenSSF Scorecard License: MIT

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 pair generates 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.yml workflow 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. read is a separate switch from send; without it, zas_list_items and zas_get_item are 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_direct takes the same read switch 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.

CodeWhat it means
not_pairedThis agent is not paired yet.
identity_corruptThe identity file on disk is damaged.
agent_revokedThe owner revoked this agent.
agent_forbiddenOnly the account owner can do that.
grant_missingThis agent has no access to that channel.
send_forbiddenThis agent cannot send to that channel.
read_forbiddenThis agent cannot read that channel.
direct_modeThat channel is in Directo mode.
not_direct_modeThat channel is not in Directo mode.
not_claimedNobody received the file within ten minutes; the offer was withdrawn.
no_offerNobody offered a file through Directo while the call waited.
offer_takenAnother device received that file first.
direct_cancelledThe offer was cancelled from the other side.
direct_failedThe Directo transfer failed in flight.
direct_not_failedThat job is not a Directo transfer that failed in flight.
file_changedThe file changed since the Directo offer.
webrtc_unavailableThe WebRTC engine could not be loaded on this machine.
fallback_unavailableReliable delivery is not available right now.
key_staleThe channel key changed; the owner refreshes it by opening Zas.
quota_exceededThe account reached its storage limit.
rate_limitedToo many sends in a row.
file_too_bigThe file is over the plan limit.
duplicateThat item is already in the channel.
not_foundThat item is not in the channel.
invalid_capThat file is no longer available.
write_failedThe download destination could not be written.
pairing_expiredThe pairing expired; pair again.
pairing_cancelledThe owner cancelled the pairing.
feature_disabledAgents are not enabled for this account yet.
upload_failedThe upload failed.
oprf_failedZas did not answer correctly while preparing the file.
networkZas cannot be reached.
sign_in_failedZas did not accept this agent session.
bad_signatureZas rejected this agent's signature; pair it again.
missing_tokenThe session token is missing; pair the agent again.
internalSomething failed inside the agent.

Every code comes back as one sentence in English, never as a stack trace.

Tools

ToolWhat it does
zas_statusSays whether this agent is paired with a Zas account, and lists the owner's channels it may send to or read from.
zas_pairPairs 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_fileSends 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_noteSends a note — plain text, or a code snippet with its language — into one of the owner's channels.
zas_send_directSends 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_fallbackAfter 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_directReceives 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_fallbackAfter a Directo receive failed in flight, downloads the encrypted copy the sender chose to store, and decrypts it to the same destination.
zas_list_itemsLists the most recent items in one of the owner's channels. Needs a grant that includes reading.
zas_get_itemFetches 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_jobsLists 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_file and zas_send_direct send any file this process can read — ~/.ssh/id_rsa and a .env included. Confirm with the owner before sending secrets, keys or credentials. Its tool description says so, so the model reads it too.
  • zas_get_item and zas_receive_direct write a new file under dest, or under the system temp directory when you leave dest out. 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:

OSPath
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 of GET /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

SettingDefaultWhat it changes
--profile <name>claude-codeWhich identity directory this process uses. Letters, digits, ., _ and -, up to 64, and it may not start with a dot.
ZAS_AGENT_HOME~/.zas/agentWhere the profile directories live.
ZAS_AGENT_TELEMETRYunset0 turns usage reporting off for this process, 1 on. It outranks the stored choice.
DO_NOT_TRACKunset1 turns usage reporting off. Read only to turn it off.
ZAS_WEB_BASEhttps://zas.redThe web app the pairing URL points at.
ZAS_API_BASEhttps://zas.red/apiThe API.
ZAS_TOKEN_BASEhttps://zas.red/anon-tokenThe challenge and token routes.
ZAS_OPRF_BASEhttps://zas.red/oprfThe blind key-derivation service.
ZAS_FIREBASE_PROJECTzas-meThe project whose Firestore the read path queries.
ZAS_FIREBASE_API_KEYthe public web keyThe 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.