Your tools are already CLIs. wires lets any agent in your organization run
them where they live: wires login says who you are (your IdP), wires services lists what you may use, and wires call runs one by name, with no
port opened and no VPN.
Experimental. wires is research code. The protocol, the file formats and the commands change without notice, nothing is kept backwards compatible, and it has had no security review. Don't use it to protect anything that matters.
You have a sqlite database on your laptop. A colleague wants to ask it a question from their agent, in the middle of their own work. Today you can send them the file, stand up a server with a port and a login somewhere, or write an MCP server for it. Each of those is a project, so usually the question goes unasked.
With wires, their agent runs this:
agent$ wires call orders-db -- "select customer, sum(total) from orders group by customer"
customer sum(total)
-------- ----------
acme 259.6
globex 329.24
initech 42.0
umbrella 999.0The query runs on your laptop, as them, and the answer comes back into their session like any other command's output.
Agents work best with command lines. Models have seen far more shell than
tool-call JSON. A CLI's output can be cut down before it reaches the model's
context (gh --jq, --json fields, head). And CLIs compose. What stops
you from sharing a CLI with everyone in an organization is three things,
and wires is those three:
- Who is calling.
wires loginsigns you in with your organization's IdP (Google, or another OIDC provider the admin trusts). The machine that runs the call checks that sign-in itself. - How they find it.
wires serviceslists the services you may call. You call one by its name. You never name a machine or handle its key. - How they reach it. Every machine is reached by its public key, over an end-to-end encrypted connection, directly or through a relay. Both sides dial out: no inbound port, no VPN.
A service can also do one thing a local CLI can't: message its caller back, after the call has ended.
One admin, one host (the workbench, holding orders.db), one agent.
# admin: create the network, say who may call what, and where it runs
admin$ wires init --client-id <your Google OAuth client id> --public-client-secret <its secret>
admin$ wires role set analyst '*@acme.com'
admin$ wires directory add workbench=2161f020032bd52d283a33399ca6364d5b41037a272bafe4b0d72eb69d5e961c
admin$ wires service add orders-db --description "Read-only SQL over orders" \
--allow analyst --host workbench
admin$ wires network # one string, the same for everyone; not secret
eyJkaXJlY3RvcmllcyI6WyIyMTYxZjAy…
# host: join, then run the services the policy assigns it
workbench$ wires id # the node id the admin named above
2161f020032bd52d283a33399ca6364d5b41037a272bafe4b0d72eb69d5e961c
workbench$ wires join <network>
workbench$ wires serve host.json # also the directory; waits for the first publish
# admin: publish the policy to the directory, once
admin$ wires policy push
wires: policy version 4: published to 1 of 1 directory(ies)
# agent: signing in is joining
agent$ wires login <network> # opens the browser
wires login: 1 service(s) you may call (policy version 4); see `wires services`
agent$ wires services
orders-db Read-only SQL over orders (analyst)
agent$ wires call orders-db -- "select count(*) from orders"
count(*)
--------
7host.json on the workbench says how orders-db runs. The host runs that
fixed command with the caller's arguments appended, with no shell:
{
"version": 2,
"services": {
"orders-db": { "command": ["sqlite3", "-safe", "-readonly", "-header", "-column", "orders.db"] }
},
"push": { "allow": ["analyst"] }
}Later, the host messages the agent, and the admin removes a person:
workbench$ wires push --to <the agent's node id> --subject build-41 -- "failed: test_orders_total"
agent$ wires inbox
2026-10-05 02:42:42Z from host 2161f020 (verified) build-41 failed: test_orders_total
admin$ wires remove alice@acme.com
agent$ wires call orders-db -- "select 1"
wires: denied by host: not admitted to this network: no role in this network matches alice@acme.com, or you were removed: ask your admin
agent$ echo $?
77The output is from a run on one machine, with wires dev-mock-idp (a
stand-in IdP built only with --features dev-mock-idp) in place of Google,
so that run's init also named the stand-in with --issuer. The network
string is cut short here, and output the tour doesn't need is left out
(join's next steps, serve's log, the push and remove receipts, the
sign-in URL login prints). The role set, directory add and service add edits each note on stderr that the new policy is stored but not yet
published; wires policy push delivers it once a directory runs.
./.scripts/demo-remote-cli.sh runs the same steps, with two hosts, and
checks every result.
A recording of ./.scripts/demo-remote-cli.sh: six keystores on one
machine over loopback, signing in through a stand-in IdP, not the
two-machine run (MP4).
The policy. The admin holds the network's root key and signs one
versioned policy: which IdPs are trusted, which roles exist (matched on the
verified identity, for example *@acme.com), which services exist, which
hosts run each, who may call each, and who is removed. Every edit is a new
signed version, published by key to the network's directories. Hosts follow
a directory and hold the whole policy. An agent's machine holds only its
view: the services its person may call.
A call. wires call looks the service up in its view and dials one of
its hosts by key. The caller tells the host nothing until it holds a word,
signed in the last 15 minutes by a directory other than that host, that the
host's copy of the policy is current. The host shows its words first; a
caller that already holds one (from its view, or an earlier call) doesn't
wait for them. So a host the admin removed is told nothing once the last
word for the old policy lapses. Then the caller sends its ID token, which is
bound to the caller's key, so it is useless from any other machine. The
host verifies the IdP's signature itself, checks its own copy of the
policy, and runs the command. There is no auth server on the call path. A
refusal is exit 77 with nothing on stdout. If a service has several hosts,
each call goes to one of them at random, and on to the next if that one
can't be reached.
The service knows who called. The command gets the caller's verified
identity in its environment: WIRES_CALLER (the verified claims, as JSON),
WIRES_CALLER_EMAIL, WIRES_ROLE, and the ID token itself
(WIRES_ID_TOKEN), so the service can apply its own rules or exchange the
token. Otherwise it gets only PATH, the locale and what host.json sets:
none of the host's keys.
Joining and removal. The network string that wires network prints
holds the root key, the first directories and the sign-in settings. A
caller joins with wires login <network>, and is in the network if a role
in the policy matches them; there is no per-machine approval. wires remove <email> removes a person, from every machine they use. (wires remove <node> takes a host or directory machine out of the network.) Every host
refuses the person's next call once the new policy reaches it, usually
within seconds, with nothing to restart and no shared secret to rotate.
Services in your own code. An app can embed the host and serve calls
in-process, in Rust (wires::Host), Python or TypeScript
(bindings/). Callers can't tell a native service from a CLI.
A webhook needs the receiver to listen, and an agent on a laptop doesn't. A
wires host sends a message to the caller's key instead, or holds it (24
hours by default) until the caller's next wires inbox. With a push
section in host.json whose roles admit the caller, each call's command
can message the caller that started it, up to 10 minutes after the call has
ended:
# in a CI job the call started, on the host
wires push --to "$WIRES_CALLER_NODE" --subject build-41 -- "failed: test_orders_total"wires inbox --wait sleeps until a message lands. An agent waiting on a
build this way spends one tool call, not a loop of status checks (numbers
below).
A recording of ./.scripts/demo-push.sh on loopback: a build the agent
started fails on the host, and wires inbox --wait wakes with the push
(MP4).
Many teams already reach remote tools through MCP clients. So every wires
service is also an MCP tool: wires mcp serves your services over stdio to
Claude Desktop and IDEs, and wires gateway is a remote MCP server that
Claude on the web adds as a custom connector, each user signing in as
themselves (docs/deployment.md § A web gateway).
The host still verifies each person's own ID token.
The bridge exists to carry existing workflows over. wires call is where
the token savings are, and the bridge should someday be unnecessary.
Our own benchmark: GitHub tasks in Claude Code, 5 tasks × 5 runs per setup, every answer correct in every setup. Runs from 2026-09-23, on an earlier version of wires (bench/REPORT.md):
| How the agent reached GitHub | Median input tokens | Cost, 25 runs |
|---|---|---|
| GitHub's MCP server (Claude Code's default, tool search on) | 21,088 | $1.87 |
| GitHub's MCP server, tool search off | 30,630 | $1.40 |
bare gh |
6,997 | $0.42 |
wires call gh |
10,539 | $0.48 |
wires call gh, the agent allowed only wires |
10,713 | $0.39 |
The gap is not tool schemas: with tool search on, the 26 tool schemas cost
about 400 tokens (about 10.2k with it off). It is results: the MCP server
returned whole API objects, while the CLI let the model pick fields first.
Bare gh did a little better than wires call gh, so the saving belongs to
the CLI; wires' job is to make the CLI reachable. One model, one MCP server,
small n, stripped-down sessions; an MCP server with field selection would
close much of the gap. All five tasks read from one service, so this says
nothing yet about composition.
Others have found the same direction. Anthropic's Code execution with MCP (2025-11-04) names tool definitions and intermediate results as the two costs, and cut one Drive-to-Salesforce workflow from 150,000 tokens to 2,000 by having the model drive tools as code. Cloudflare's Code Mode (2025-09-26): "LLMs have seen a lot of code. They have not seen a lot of 'tool calls'." Scalekit's GitHub benchmark (2026-03-11) measured 4 to 32 times fewer tokens for the CLI than for GitHub's MCP server over 75 runs, and argues MCP is still needed for per-user authorization, tenant isolation and audit trails. wires is aimed at the first two; it has no audit trail.
Waiting on a mock CI build, 5 runs per setup, also from 2026-09-23 (bench/push/REPORT.md):
| How the agent waited | Turns | Input tokens (median) | Reaction |
|---|---|---|---|
| Polling (60 s → 300 s build) | 9 → 13 | about 28k → 39k | 20–178 s |
wires inbox --wait |
4 | 15.4k, flat | about 2 s |
Checking wires inbox on a loop costs what polling costs; the saving comes
only from waiting on the push.
A remote MCP server listens at a URL the client can reach, and is its
own OAuth resource server. Its results are whatever the server returns; the
protocol has no client-side field selection. It reaches a client only over
a stream the client holds open. wires runs a CLI, so the agent trims output
with the CLI's own flags or wires call --jq/--head/--max-bytes; one
admin-signed policy covers every service; wires services is a per-person
catalog; and a host can reach an agent that isn't connected. wires covers
only tools: no MCP prompts or resources.
SSH, Tailscale SSH and Teleport solve a different problem. They give a
person a login on a machine. wires gives a person a named service, wherever
it runs: the caller asks for orders-db, the admin decides which hosts run
it, and the caller has no account and no shell on any of them.
Tailscale gives the agent's machine a network route to the host, scoped by machine and port, and whatever listens there still does its own sign-in. A wires host opens no inbound port: an agent reaches only the services the policy lets its person call, checked against their IdP sign-in on every call.
wires keeps no log of calls for you. The host writes one ordinary log line per call (service, caller, verified email, role, exit code) to its own output, and nothing else records calls.
- The admin doesn't approve each machine. Anyone your IdP verifies (with a verified email) and a role matches is in from any machine. A phished sign-in that binds an attacker's key would be admitted.
- The ID token is the only credential. Google's last about an hour, so a
caller runs
wires loginagain each hour. Disabling someone at the IdP cuts them off within one token's life. - Every service a caller calls gets their ID token, a bearer credential until it expires, and a service's command runs as the host's own Unix user unless the operator sets up another.
- Hosts share nothing but the policy. Calls spread at random across a service's hosts, so a service that keeps state between calls (in memory or on its machine's disk) answers from whichever host the call landed on, and a push waits on the host that sent it.
- Hosts and directories hold the whole policy (roles, services, host keys, removals). A caller holds only its view, but a directory sees who asks for which view.
- Calls need a directory. A caller sends a host nothing without a word
from a directory other than that host, signed in the last 15 minutes
(
fresh_secs), that the host's policy is current. With every directory down, calls stop within 15 minutes and fail with exit 1. A directory must also be running to change the policy, remove someone or fetch a caller's view. - Removing a host takes up to 15 minutes. A host the admin took off a
service can still receive the ID token and arguments of a caller whose view
is older than the edit, until the last word a directory signed for the old
policy lapses; a directory that missed the edit keeps vouching for the old
policy until
wires policy pushreaches it. In a one-machine network (one workbench is host and directory) the machine's own word counts, so removing it holds only at the policy's expiry; a second directory makes removal hold for callers that know of it. - The policy expires 90 days after the last edit by default, and nothing
renews it on its own. Removals don't expire; they stay until
wires restore. - Callbacks go to a caller's key, not yet only to the caller that asked (card 31).
- Only Google has been tested as the IdP. The web gateway listens over HTTPS and holds each signed-in user's token until it expires.
The full list: docs/usage.md § Known trade-offs.
- docs/usage.md: the roles, a walkthrough with output,
host.json, locking an agent down towires, and the commands by role. - docs/examples.md: eight CLIs that teams reach today
through vendor MCP servers (
aws,gcloud,kubectl,gh,wrangler,vercel,supabase,stripe) as wires services, each with its wrapper, its vendor setup and what it doesn't cover. - docs/protocol.md: the protocol, as the code does it.
- docs/executive-summary.md: the product on one page.
- docs/blog/introducing-wires.md: the announcement post.
make install # wires on your PATH (~/.cargo/bin)
./.scripts/demo-remote-cli.sh # the narrated loopback demo (--quiet: checks only)MIT.

