Every freelancer and agency has done this. A client hires you to fix their website or run their social media. Your first message is "can you share the login?" and a few minutes later a GoDaddy password is sitting in a WhatsApp chat, a Cloudflare login is in someone's email, and an Instagram password is in a screenshot.
I've asked for those passwords many times. I'm a freelance developer and marketer from Hisar, India, and for most of my clients there was no better way to do the work. Sharing a password felt wrong, but nobody offered an alternative that wasn't a pile of setup.
Then I started doing more of this work with an AI agent, and the problem got worse. An agent that can edit DNS or schedule posts needs credentials, and now those credentials end up in prompts and tool configs. The client has no way to see what the agent is about to do, and I have no clean way to prove they said yes.
For the Flip the Switch hackathon I built Keystead to fix this. The idea is short: my AI agent does the work, the client's keys stay with the client, and nothing runs until the client types approve KS-0001 in a shared chat room.
What Switch is
Switch is an open-source project that puts AI agents into team chat. You run a Switch server, and agents join rooms in Slack, Discord, Teams, Telegram or Mattermost. Each agent belongs to a person and acts with that person's access. A room is just a shared conversation where people and agents talk to each other.
The detail that mattered for me is that agents in a room can belong to different people. Most agent tooling I've used assumes one team and one owner. Switch doesn't, and that's what made Keystead possible.
The idea
Keystead puts two agents in one private room.
-
@agencyis my Claude Code agent. It does the actual thinking: reading the client's request, deciding what change is needed, and writing it up. It has no client credentials at all. -
@keysteadis a small agent that runs on the client's side. It holds the client's keys in an encrypted vault. It has no LLM. It's plain Python.
A typical exchange looks like this:
client: can you point the new landing page to the new server? @agency: Proposing a DNS change: A record shop.example.com -> 203.0.113.10 (TTL 300) @keystead: Proposal KS-0001 Change: update A record shop.example.com -> 203.0.113.10 Policy: requires approval Expires: 30 minutes Reply "approve KS-0001" to run it. client: approve KS-0001 @keystead: Done. Record updated. Receipt saved, audit entry #1. The client never logs into anything and never shares a secret. They see exactly what will happen, in a chat they already use, and they say yes in one word.
Why the client's agent has no LLM
This is the design decision I'd defend the hardest.
If the gate is an LLM, someone can talk it into things. A prompt injection in an email, a clever message in the room, or a confused instruction could all push it to approve something it shouldn't. A gate that only reads structured proposals and exact approval commands can't be talked into anything, because there's nothing in it to persuade.
So the split is deliberate. The agency's agent is smart and has no keys. The client's agent has the keys and isn't smart. Neither one alone can do damage.
Rules, not vibes
Before a proposal even reaches the client, @keystead checks it against a policy file. The first matching rule wins, and the default is deny:
rules: - action: dns.update zone: example.com decision: approve # client must say yes - action: dns.delete decision: deny # never, even if asked - action: social.post decision: approve limit_per_day: 3 Then a few checks keep the approval honest:
- Each proposal gets an ID like
KS-0001, and the approval is tied to a hash of the exact payload. If anything changes after the client saw it, the approval doesn't apply. - Approvals are single-use and expire after 30 minutes.
- The approver has to be the client's own account in the room. Approvals from platform messages or from an agent speaking on someone's behalf are rejected.
- Secrets are redacted from anything the gate posts back to the room.
- Every action goes into a hash-chained audit log, so editing an old entry breaks the chain.
Setting up Switch
Getting Switch running took me about ten minutes. I ran the server with Docker, opened the Console, and created a room. From there it's two things: connect your own agent, and register a custom one.
The agency side
My agent is Claude Code connected through Switch. The room is defined with a template so the setup is repeatable for every new client:
# keystead-room.template.yaml (simplified) params: client_name: required: true agents: - name: agency kind: claude-code room: name: "Keystead: {{ client_name }}" kickoff: | Propose changes as a JSON block. Never ask for credentials. The kickoff message is the important part. It tells the agent that its only job is to propose changes in a fixed format, and that it must never ask for or handle credentials. If it needs something done, it writes a proposal and waits.
The client side
@keystead is a custom Switch agent. The protocol is simple enough that this is a small program: register once, listen to a stream of events, and post back over HTTP.
# simplified sketch of the gate's loop register_agent(token=REGISTRATION_TOKEN) for event in sse_stream(room): if is_proposal(event): decision = policy.check(event.proposal) if decision == "approve": post_card(event.proposal) elif is_approval(event) and approver_matches(event): run_connector(find_proposal(event)) post_receipt() The connectors are thin wrappers around real APIs: Cloudflare DNS to start, and a social posting service for scheduled posts. Each one only runs after the gate has checked the policy, the proposal ID, the payload hash and the approver.
One gotcha worth knowing
Switch has a built-in permission prompt: allow once, allow for the session, or reject. I first assumed I could use it as the approval step. That would be a mistake for this use case.
That prompt is meant for a single owner approving their own agent's tool use. It isn't a cross-organization approval, because calls to an agent's registered tools don't go through it. If you're building anything where one person's agent acts on another person's behalf, build the approval step yourself and tie it to a specific identity. That's why Keystead uses its own approve KS-NNNN command.
What I liked, and what I'd change
Rooms with agents owned by different people are the best part of Switch, and I think it's underrated. It turns a chat room into a shared workspace between companies, not only inside one team. The agent protocol is also small enough that a custom agent is an afternoon of work, not a project.
On the wish list: a first-class way to mark a message as a formal approval that only a named person can give, and clearer docs on adding a client-owned agent to a room template. I'd also like to see per-room agent permissions in the Console.
What's next
Keystead covers DNS and social posting today. The pattern works for anything an agency does on a client's behalf: ad accounts, email tools, hosting panels, analytics access. Each is a new connector behind the same gate.
The same idea could also work as a hosted service for agencies, with each client getting their own gate. I haven't built that, and if I go that way I'll need to check Switch's license terms around commercial use first.
Try it
Built for the Flip the Switch hackathon by WeMakeDevs. If you've ever pasted a client's password into a chat, I'd like to hear how you handle it now.

