<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dikshant Jangra</title>
    <description>The latest articles on DEV Community by Dikshant Jangra (@dikshantjangra).</description>
    <link>https://dev.to/dikshantjangra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4232586%2F84a6b005-7e17-4d7c-806a-3b3a109dff19.jpg</url>
      <title>DEV Community: Dikshant Jangra</title>
      <link>https://dev.to/dikshantjangra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dikshantjangra"/>
    <language>en</language>
    <item>
      <title>I stopped asking clients for their passwords. Here's what I built on Switch instead. published: true</title>
      <dc:creator>Dikshant Jangra</dc:creator>
      <pubDate>Sat, 07 Nov 2026 18:04:13 +0000</pubDate>
      <link>https://dev.to/dikshantjangra/i-stopped-asking-clients-for-their-passwords-heres-what-i-built-on-switch-instead-published-2ap8</link>
      <guid>https://dev.to/dikshantjangra/i-stopped-asking-clients-for-their-passwords-heres-what-i-built-on-switch-instead-published-2ap8</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For the Flip the Switch hackathon I built &lt;strong&gt;Keystead&lt;/strong&gt; 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 &lt;code&gt;approve KS-0001&lt;/code&gt; in a shared chat room.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Switch is
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Keystead puts two agents in one private room.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@agency&lt;/code&gt;&lt;/strong&gt; is 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@keystead&lt;/code&gt;&lt;/strong&gt; is 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical exchange looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client:    can you point the new landing page to the new server?
@agency:   Proposing a DNS change:
           A record  shop.example.com  -&amp;gt;  203.0.113.10  (TTL 300)
@keystead: Proposal KS-0001
           Change:  update A record shop.example.com -&amp;gt; 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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the client's agent has no LLM
&lt;/h2&gt;

&lt;p&gt;This is the design decision I'd defend the hardest.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules, not vibes
&lt;/h2&gt;

&lt;p&gt;Before a proposal even reaches the client, &lt;code&gt;@keystead&lt;/code&gt; checks it against a policy file. The first matching rule wins, and the default is deny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns.update&lt;/span&gt;
    &lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;
    &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;approve&lt;/span&gt;        &lt;span class="c1"&gt;# client must say yes&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns.delete&lt;/span&gt;
    &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deny&lt;/span&gt;           &lt;span class="c1"&gt;# never, even if asked&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;social.post&lt;/span&gt;
    &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;approve&lt;/span&gt;
    &lt;span class="na"&gt;limit_per_day&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a few checks keep the approval honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each proposal gets an ID like &lt;code&gt;KS-0001&lt;/code&gt;, 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.&lt;/li&gt;
&lt;li&gt;Approvals are single-use and expire after 30 minutes.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Secrets are redacted from anything the gate posts back to the room.&lt;/li&gt;
&lt;li&gt;Every action goes into a hash-chained audit log, so editing an old entry breaks the chain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up Switch
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agency side
&lt;/h2&gt;

&lt;p&gt;My agent is Claude Code connected through Switch. The room is defined with a template so the setup is repeatable for every new client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# keystead-room.template.yaml (simplified)&lt;/span&gt;
&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;client_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agency&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-code&lt;/span&gt;
&lt;span class="na"&gt;room&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Keystead:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;client_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;span class="na"&gt;kickoff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Propose changes as a JSON block. Never ask for credentials.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client side
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@keystead&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# simplified sketch of the gate's loop
&lt;/span&gt;&lt;span class="nf"&gt;register_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REGISTRATION_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sse_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_proposal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;post_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;is_approval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;approver_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;run_connector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;find_proposal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;post_receipt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  One gotcha worth knowing
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;approve KS-NNNN&lt;/code&gt; command.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I liked, and what I'd change
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/DikshantJangra/KeyStead" rel="noopener noreferrer"&gt;https://github.com/DikshantJangra/KeyStead&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
