<?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: GetPochi</title>
    <description>The latest articles on DEV Community by GetPochi (@getpochi).</description>
    <link>https://dev.to/getpochi</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3607422%2Fed1a20c6-0f52-43a9-8fc0-079c040296b9.png</url>
      <title>DEV Community: GetPochi</title>
      <link>https://dev.to/getpochi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/getpochi"/>
    <language>en</language>
    <item>
      <title>Five Practical Tips to Save Token Consumption with Pochi</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Wed, 04 Mar 2026 08:17:52 +0000</pubDate>
      <link>https://dev.to/getpochi/five-practical-tips-to-save-token-consumption-with-pochi-1jag</link>
      <guid>https://dev.to/getpochi/five-practical-tips-to-save-token-consumption-with-pochi-1jag</guid>
      <description>&lt;p&gt;The usual experience with coding agents is predictable - they start out sharp, then slowly become confused, verbose, and expensive. Instructions keep piling up, tools accumulate, and failed attempts linger in the conversation. By the time token costs start hitting the roof, the agent already feels harder to work with.&lt;/p&gt;

&lt;p&gt;At that point, most teams reach for the usual fixes: improve the prompts, avoid the biggest models for small tasks, and aggressively cache tool calls. While all of these help, they come with their own overhead of constant tuning and close monitoring - and even then, they rarely address the root cause of runaway token usage.&lt;/p&gt;

&lt;p&gt;This post breaks down practical workflow patterns you can apply to address common sources of context bloat, explains the principles behind them, and shows how Pochi supports these behaviors in day-to-day work. If you’ve ever felt like an agent got worse the longer you worked with it, these patterns are likely why, and how Pochi helps you fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Compact context aggressively as noise accumulates
&lt;/h2&gt;

&lt;p&gt;Token usage grows over time as conversations accumulate failed attempts and abandoned approaches. This context debt increases token usage and degrades response quality, making agents more verbose and error-prone.&lt;/p&gt;

&lt;p&gt;To solve this, Pochi periodically allows you two options to &lt;strong&gt;&lt;a href="https://docs.getpochi.com/auto-compact/" rel="noopener noreferrer"&gt;compact the context&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compact Task:&lt;/strong&gt; This summarizes the task context and replaces long conversational history with a concise, up-to-date representation of intent and state. Applicable when you want to stay in the same task and continue the conversation with the condensed context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/oT2hkErhMm8"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create a New Task with Summary:&lt;/strong&gt; This creates a clean task with a summary of the previous conversation, helping you avoid hitting context limits while keeping all relevant information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/E9s8Gfxw5BY"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;These mechanisms are especially useful during long debugging sessions, iterative refactors, or tasks with multiple rounds of clarification. &lt;/p&gt;

&lt;p&gt;In these cases, the majority of the conversation history becomes irrelevant once a direction is chosen. Compacting ensures the agent doesn’t keep paying for that history over and over again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a task gets long, compact the context regularly (e.g., after 3–5 iterations).&lt;/li&gt;
&lt;li&gt;Keep only the essential state and intent.&lt;/li&gt;
&lt;li&gt;If the direction changes, create a new task with a summary of the previous conversation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. Attach intent to code instead of explaining it in chat
&lt;/h2&gt;

&lt;p&gt;Explaining code changes in plain chat is one of the fastest ways to burn tokens. Each time you prompt the agent with queries like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt:
- “Actually, change this part…”
- “No, not that file , the other one”
- “I meant refactor this logic, not rewrite it”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model has to re-read large parts of the context, reconstruct what changed, and infer your intent all over again. This kind of repetition adds up quickly.&lt;/p&gt;

&lt;p&gt;Pochi avoids this by attaching intent directly to code through &lt;strong&gt;Edits&lt;/strong&gt; and &lt;strong&gt;Reviews&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.getpochi.com/edits/" rel="noopener noreferrer"&gt;Edits&lt;/a&gt;&lt;/strong&gt; track the exact diffs you introduce locally while iterating. If you tweak a variable, adjust logic, or partially rewrite a block, Pochi includes only those changes in the agent’s context the next time you send a prompt. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/UFIprUSpDio"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.getpochi.com/reviews/" rel="noopener noreferrer"&gt;Reviews&lt;/a&gt;&lt;/strong&gt;, on the other hand, let you leave inline comments directly on generated code. Instead of re-explaining issues in chat, you comment on specific lines and batch that feedback into a single, focused update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/Qrd2N1gXtaY"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use edits to track local diffs during iteration.&lt;/li&gt;
&lt;li&gt;Attach intent directly to code using inline comments.&lt;/li&gt;
&lt;li&gt;Batch feedback into a single update instead of multiple chat messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Isolate intent early with subagents and forks
&lt;/h2&gt;

&lt;p&gt;Token usage often spikes when multiple ideas compete in the same context. You start with one goal, explore a few approaches, abandon some, and finally pivot to another direction. In this case, the agent is continuously juggling multiple lines of intent. Even with compaction, the model still has to reconcile what you meant before with what you want now.&lt;/p&gt;

&lt;p&gt;Language models are optimized for coherent, single-threaded intent. When a task mixes multiple implementation strategies, the model keeps all of that alive in context, even if only one direction is still relevant. &lt;/p&gt;

&lt;p&gt;The answer is isolation. Separate tasks mean separate contexts, and separate contexts mean fewer tokens spent reconciling unrelated ideas.&lt;/p&gt;

&lt;p&gt;Pochi supports this through task forking and subagents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.getpochi.com/developer-updates/#-features-6" rel="noopener noreferrer"&gt;Forking a task&lt;/a&gt;&lt;/strong&gt; creates a new task that starts from the current code state but does not inherit conversational noise. It’s ideal when you want to try a different approach or explore an alternative implementation without dragging prior reasoning along.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/aZFJ-vjZe5o"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.getpochi.com/custom-agent/" rel="noopener noreferrer"&gt;Subagents&lt;/a&gt;&lt;/strong&gt; allow focused exploration within the same repository while keeping contexts separate. Each subagent works with a clean, bounded scope instead of accumulating unrelated history. In practice, this kind of isolation can lead to dramatic token savings. Developers running large, multi-step workflows often split work across multiple subagents, each with its own narrow instruction set. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5x01w52csld87chujcr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5x01w52csld87chujcr.png" alt="Subtasks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When you explore multiple approaches, create a fork or a subagent.&lt;/li&gt;
&lt;li&gt;Keep each task focused on one implementation strategy.&lt;/li&gt;
&lt;li&gt;Use separate contexts for separate goals.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Scope MCP servers per task to shrink the action space
&lt;/h2&gt;

&lt;p&gt;Before an agent attempts to solve a problem, it evaluates what it can do with every enabled tool. Each additional MCP server expands the action space the model must reason over. Even if a tool is never used, the model still spends tokens evaluating whether it is relevant, how it compares to other options, and when it applies.&lt;/p&gt;

&lt;p&gt;For example, if a task only requires database access, exposing ten additional MCP servers adds unnecessary reasoning overhead. More options mean more branches to evaluate, which translates directly into higher token usage.&lt;/p&gt;

&lt;p&gt;Scope MCP servers per task so the agent only reasons about tools relevant to the task. Only selected servers are loaded into context, contribute tool definitions, and influence model reasoning. All other tools are invisible to the agent.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/FC1DcODUkyw"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps to follow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before starting a task, decide which MCP servers are necessary.&lt;/li&gt;
&lt;li&gt;Disable all MCP servers not required for this task.&lt;/li&gt;
&lt;li&gt;If the task changes, immediately rescope the tool set.&lt;/li&gt;
&lt;li&gt;Keep a task template for common workflows (DB, infra, testing, etc.).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. When execution dominates, move heavy data processing out of the model
&lt;/h2&gt;

&lt;p&gt;Once you’ve scoped tools correctly, the next source of runaway token usage often shows up during execution, typically inside MCP-backed workflows.&lt;/p&gt;

&lt;p&gt;The prompt may be short and the tool choice correct, yet execution tokens spike because large volumes of raw data are streamed into the model. &lt;/p&gt;

&lt;p&gt;Tool calls may return hundreds or thousands of rows, which then get streamed into the model as large JSON payloads. At that point, execution tokens dwarf everything else.&lt;/p&gt;

&lt;p&gt;The failure mode is asking the model to reason over this raw data. For example, we asked Pochi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: How many failed orders &lt;span class="k"&gt;do &lt;/span&gt;we have?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first attempt, the agent queried the database, fetched all matching rows, and streamed them into the model so it could filter and count them. &lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/4b9F6utfiWM"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;The answer was correct, but thousands of records flowed through the context just so the model could compute a single number. &lt;/p&gt;

&lt;p&gt;A better approach is to separate what needs to be computed from how it is computed. Instead of reasoning over data, the model should generate code that performs the computation and returns only the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Please write a small script that queries the database, filters failed orders and only returns the final count. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/S9h9ToiGq6s"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Pochi generated the following script:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_failed_orders&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Assuming the database file path based on common patterns if it were local, 
&lt;/span&gt;    &lt;span class="c1"&gt;# but since I have execute_sql tool, I will demonstrate how to do it via SQL.
&lt;/span&gt;    &lt;span class="c1"&gt;# For a standalone script, it would typically connect to a DB.
&lt;/span&gt;
    &lt;span class="c1"&gt;# This is a conceptual script that would be used in an environment where 
&lt;/span&gt;    &lt;span class="c1"&gt;# the DB connection is established.
&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) as failed_count FROM orders WHERE status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# In this specific environment, we use the provided tools to interact with the DB.
&lt;/span&gt;    &lt;span class="c1"&gt;# If this were a real Python script for the user:
&lt;/span&gt;    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    conn = sqlite3.connect(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;database.db&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
    cursor = conn.cursor()
    cursor.execute(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM orders WHERE status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
    count = cursor.fetchone()[0]
    print(f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total failed orders: {count}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
    conn.close()
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Since I'm tasked to write a script, I'll provide a clean Python script.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Querying failed orders count...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Placeholder for actual DB execution logic
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_failed_orders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SQL to execute: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when prompted again to fetch the number of failed orders, the model never sees the raw records. Only a short summary enters the context, dropping execution tokens from tens of thousands to a few hundred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just run a &lt;code&gt;COUNT(*)&lt;/code&gt; query?
&lt;/h3&gt;

&lt;p&gt;At this point, it’s reasonable to think that the agent can run the SQL query directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"SELECT COUNT(*) as failed_count FROM orders WHERE status = 'failed';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why do we need to get the agent to write a separate script? &lt;/p&gt;

&lt;p&gt;And you’re right. Expect that agents often choose the least expensive path. Even when a database can do aggregation, agents frequently fall back to pull-and-process patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where token usage explodes, for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema uncertainty:&lt;/strong&gt; If the agent isn’t confident about column names, enums, joins, indexes, it plays it safe by fetching rows and reasoning in-text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous instructions:&lt;/strong&gt; If the prompt says: “Find refunded orders and tell me how many there are”, the agent may fetch records first, inspect fields, and thenthen count, Instead of jumping straight to &lt;code&gt;COUNT(*)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool abstraction:&lt;/strong&gt; Many MCP database tools expose &lt;code&gt;run query&lt;/code&gt;, &lt;code&gt;fetch rows&lt;/code&gt;, but don’t strongly bias the model toward aggregation-first queries. So the model takes the path it can reason about most reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-step reasoning:&lt;/strong&gt; If the question is slightly more complex: “How many refunded orders from customers who signed up last quarter?”. The agent might fetch orders, fetch users, join in its head and then count. That’s almost guaranteed to stream a lot of data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Databases are cheap at filtering and counting while language models are not. The solution is simple - let the model decide what to compute, and let code handle how the computation happens. Only the final result should enter the context. Having a script lets us review the code and make sure that it runs the same computation every time the same prompt is called.&lt;/p&gt;

&lt;p&gt;This keeps execution costs predictable, even when working with large datasets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect when tool results exceed ~100 rows.&lt;/li&gt;
&lt;li&gt;Instead of asking the model to reason over raw data, ask it to generate code that computes the result.&lt;/li&gt;
&lt;li&gt;Return only the final result to the model (summary, count, aggregation).&lt;/li&gt;
&lt;li&gt;Use aggregation-first queries when possible.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If there’s one theme across all five guides, it’s that token usage is shaped long before a prompt is sent.&lt;br&gt;
Most token blowups don’t come from bad prompts or choosing the wrong model. They come from workflows that allow too much context, too many tools, and too many competing ideas to accumulate in the same place.&lt;/p&gt;

&lt;p&gt;When each task has a clear goal, well-suited tools, and a clean context, the agent doesn’t have to waste tokens reconciling noise. It can converge faster, reason more clearly, and produce better results at lower cost.&lt;/p&gt;

&lt;p&gt;At Pochi, this philosophy is baked into the product. The goal isn’t to make you think about tokens because we tailor an experience that naturally keeps context small, intent clear, and costs predictable.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>beginners</category>
      <category>productivity</category>
      <category>learning</category>
    </item>
    <item>
      <title>A safe way to let coding agents interact with your database (without prod write access)</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Tue, 24 Feb 2026 03:12:47 +0000</pubDate>
      <link>https://dev.to/getpochi/a-safe-way-to-let-coding-agents-interact-with-your-database-without-prod-write-access-49jm</link>
      <guid>https://dev.to/getpochi/a-safe-way-to-let-coding-agents-interact-with-your-database-without-prod-write-access-49jm</guid>
      <description>&lt;p&gt;We &lt;strong&gt;&lt;a href="https://dev.to/getpochi/how-to-give-coding-agents-access-to-ssh-and-databases-without-breaking-production-3f2e"&gt;previously examined common approaches&lt;/a&gt;&lt;/strong&gt; teams use to protect production databases (i.e. command allowlists, SQL filters, and manual approval workflows) and why they fail in the presence of autonomous agents.&lt;/p&gt;

&lt;p&gt;The primary reason is that agents "work really hard" - they often route around these restrictions to deliver the results with any possible execution surface (shell, file system, runtime).&lt;/p&gt;

&lt;p&gt;This tutorial demonstrates how to grant database access in Pochi without exposing production credentials or enabling uncontrolled writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Agents must never execute arbitrary code against production systems. At the same time, agents are most useful when they can read and write data to iterate quickly. The challenge is doing this safely.&lt;/p&gt;

&lt;p&gt;We’ll walk through multiple access tiers, explain their security tradeoffs, and show how to progressively increase agent autonomy without expanding the production attack surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before we start, you’ll need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://nodejs.org/en/download" rel="noopener noreferrer"&gt;Node.js installed&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;PostgreSQL running&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Before following along, do note that this tutorial is a demo. Don’t expose credentials in real systems and always use secrets managers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tier 1: Read-only inspection
&lt;/h2&gt;

&lt;p&gt;To make sure that an agent never connects to the database directly, a common first step is to provide &lt;code&gt;read-only&lt;/code&gt; access via a tool interface. The tool service owns the database credentials and strictly constrains the queries that can be executed.&lt;/p&gt;

&lt;p&gt;For our demo, we’ll use an MCP HTTP service that exposes a fixed set of &lt;code&gt;read-only&lt;/code&gt; tools. Production data will be accessible only through this interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a read-only Postgres role
&lt;/h3&gt;

&lt;p&gt;We’ll use a Postgres database. To create one, run the following command in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="n"&gt;shop_sandbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we create a dedicated database role whose permissions enforce read-only access at the database layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'readonly'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;shop_sandbox&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Continue over to add a new table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we proceed to add data. Here's a sample to get you started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'2 days'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: MCP Server (&lt;code&gt;read-only&lt;/code&gt; tools)
&lt;/h3&gt;

&lt;p&gt;Next, we implement a minimal MCP server (&lt;code&gt;server.js&lt;/code&gt;) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/* ---------------- DB (PRODUCTION READ-ONLY) ---------------- */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shop_sandbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="cm"&gt;/* ---------------- MCP HTTP Server ---------------- */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;initialize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;protocolVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2024-11-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;serverInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod-readonly-db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ping&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;})),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Unknown&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Method not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3333&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Prod&lt;/span&gt; &lt;span class="nx"&gt;MCP&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:${PORT}/mcp);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this MCP server, we expose a narrowly scoped tool called &lt;code&gt;recent_orders&lt;/code&gt; that allows us to filter the most recent orders in a given time period.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;recent_orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;since_hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
        SELECT id, user_id, status, total, created_at
        FROM orders
        WHERE created_at &amp;gt;= now() - ($1 || ' hours')::interval
          AND ($2::text IS NULL OR status = $2)
        ORDER BY created_at DESC
        LIMIT 50
      `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;since_hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Configure Pochi
&lt;/h3&gt;

&lt;p&gt;Now we configure Pochi to use the MCP. For that, let's add to &lt;code&gt;config.jsonc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"prod-readonly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3333/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhixx96v8orln857g6zzg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhixx96v8orln857g6zzg.png" alt="recent-orders" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the agent’s perspective, the only available interface to production data is the MCP tool API. To test this theory, let’s give the agent a &lt;code&gt;read-only&lt;/code&gt; prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Show failed orders &lt;span class="k"&gt;in &lt;/span&gt;the last 24 hours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;As expected, the agent invokes the &lt;code&gt;recent_orders&lt;/code&gt; tool and gets the corresponding output. Since there was no write operation involved, there was no direct database access. &lt;/p&gt;

&lt;p&gt;So far, this looks safe.&lt;/p&gt;

&lt;p&gt;Next, let’s ask the agent to modify some data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Mark this order as refunded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since there is no MCP tool capable of performing writes, the expected behaviour is for execution to fail. &lt;/p&gt;

&lt;p&gt;Instead of failing, the agent searches for alternative execution paths to complete the task when its first few attempts to do a write operation fails. It searches the code for database-related code, generates a small &lt;code&gt;Node.js&lt;/code&gt; script using the &lt;code&gt;pg&lt;/code&gt; client and executes it through the shell. This ends up updating the database directly.&lt;/p&gt;

&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;If you look closely, the agent simply completed the task using an execution path that still existed because we left that gap open. &lt;/p&gt;

&lt;p&gt;From this, we conclude that since the agent can be given shell access, database credentials, or the ability to construct queries, the security boundary still lives inside the model rather than the system.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Disable execution surface
&lt;/h3&gt;

&lt;p&gt;To actually make the setup read-only, two additional controls can be applied. First, we explicitly revoke database writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, we disable execution permissions in the editor. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3wjkk00pevilwvx0n8m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3wjkk00pevilwvx0n8m.png" alt="shell-production" width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach removes the entire class of risk. From Pochi’s standpoint, this means the agent cannot run any shell commands, write files, or execute programs. The only remaining interface was the MCP tool API, which is equivalent to calling a standard, credential-isolated API to query production data. &lt;/p&gt;

&lt;p&gt;Following this, the read requests continued to work as expected from before. The agent used the MCP tools and returned the proper output. But now, when asked again to do a write operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Mark this order as pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent could still reason about how to perform updates, and could still propose code in the editor, but it no longer had the ability to apply or execute those changes without explicit human approval. The task remained incomplete by design. &lt;/p&gt;

&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;That being said, production was still accessible through the same execution surface, requiring constant human oversight. This could lead to problems where a human might blindly click ‘Save’ or settings may have auto-approve write enabled by default. &lt;/p&gt;

&lt;p&gt;While this may appear operational, it is strongly recommended to avoid granting even read access to production databases due to the unpredictable nature of agents. Agents are non-deterministic by design, and production systems should not be exposed to that uncertainty.&lt;/p&gt;

&lt;p&gt;But the challenge remains that many legitimate tasks still require writes. This leads us to the next tier of access.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tier 2:  Safe writes via clone + pipeline (demo)
&lt;/h2&gt;

&lt;p&gt;Granting an agent direct write access to production is dangerous. Even if you try to enforce human approvals, the agent can still find ways to bypass restrictions if it has any execution surface.&lt;/p&gt;

&lt;p&gt;The safer approach is to strictly separate reasoning from execution. &lt;/p&gt;

&lt;p&gt;For this we'll use an &lt;strong&gt;Isolated Work Environment (IWE)&lt;/strong&gt;. The agent can generate and test migration scripts in a writable clone of the database (IWE), but the production database remains locked down. Once the migration is validated on the clone, the same script is applied to production through the normal deployment pipeline, with human approval and rollback controls.&lt;/p&gt;

&lt;p&gt;Let’s have a look at what this looks like in a complete flow.&lt;/p&gt;

&lt;p&gt;Continuing our example from before, we found that many paid orders in production were missing the total amount value. The correct total for an order should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We want to backfill all orders where the total &lt;code&gt;IS NULL&lt;/code&gt;. At the same time, we also do not want to give the agent write access to production. &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Architecture
&lt;/h3&gt;

&lt;p&gt;In order to enforce isolation, we’ll run two separate MCP services. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production MCP:&lt;/strong&gt; This will be connected to our existing database, i.e. &lt;code&gt;shop_sandbox&lt;/code&gt;, which has a &lt;code&gt;read-only&lt;/code&gt; DB role&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation MCP:&lt;/strong&gt; This will be connected to a clone of shop_sandbox named &lt;code&gt;shop_validate&lt;/code&gt; and will have a &lt;code&gt;write-capable&lt;/code&gt; DB role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each MCP server will have its own database credentials and expose its own tool interface. From the agent’s point of view, the only way to interact with a database is by calling tools exposed by whichever MCP servers are enabled for the task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add a production inspection tool
&lt;/h3&gt;

&lt;p&gt;As seen earlier,  &lt;code&gt;shop_sandbox&lt;/code&gt; is locked down. The agent can only query it via MCP read-only tools. The database role for this MCP has only &lt;code&gt;SELECT&lt;/code&gt; permission, and shell access is disabled in agent tasks. &lt;/p&gt;

&lt;p&gt;Previously, we only exposed the &lt;code&gt;recent_orders&lt;/code&gt; tool via this MCP. Now we’ll introduce the &lt;code&gt;orders_missing_totals&lt;/code&gt; tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt; &lt;span class="nx"&gt;orders_missing_totals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Find orders with NULL totals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nx"&gt;SELECT&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;created_at&lt;/span&gt;         &lt;span class="nx"&gt;FROM&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;         &lt;span class="nx"&gt;WHERE&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="nx"&gt;IS&lt;/span&gt; &lt;span class="nx"&gt;NULL&lt;/span&gt;         &lt;span class="nx"&gt;ORDER&lt;/span&gt; &lt;span class="nx"&gt;BY&lt;/span&gt; &lt;span class="nx"&gt;created_at&lt;/span&gt; &lt;span class="nx"&gt;DESC&lt;/span&gt;        
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpxuws6bb4w242wnk28s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpxuws6bb4w242wnk28s.png" alt="production-mcp-both-tools" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To check if everything works as expected, let’s run a sample prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Show orders missing total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;As seen, the agent can inspect how many orders are missing a total field, but it cannot modify anything on the production database. &lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Create a writable clone
&lt;/h3&gt;

&lt;p&gt;Next, we create a writeable clone of  &lt;code&gt;shop_sandbox&lt;/code&gt;  named  &lt;code&gt;shop_validate&lt;/code&gt; with the same schema and data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;pg_dump&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="n"&gt;apple&lt;/span&gt; &lt;span class="n"&gt;shop_sandbox&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="n"&gt;shop_validate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a write-capable role for validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'writer'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grant database and schema access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Create&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;write&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;capable&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'writer'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;Grant&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;shop_validate&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Setup Validation MCP
&lt;/h3&gt;

&lt;p&gt;Add the new server to &lt;code&gt;config.jsonc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"validate-write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3334/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This new MCP will be our environment for the agent to perform writes. It will expose two tools: &lt;code&gt;execute_sql&lt;/code&gt; and &lt;code&gt;run_migration_file&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;execute_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Execute raw SQL against validation database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;run_migration_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Run SQL migration file against validation database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Migration&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="na"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rowCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13vie6e18bqqe4nybzso.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13vie6e18bqqe4nybzso.png" alt="validate-mcp-both-tools" width="800" height="707"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This way, we give Validation MCP the ability perform write operations while Production MCP only has read access. For the purpose of this blog, this is how we achieved environment isolation by routing the agent to different tool backends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Plan (Pochi Plan Mode)
&lt;/h3&gt;

&lt;p&gt;Next, we will use Pochi’s plan mode to create a migration plan. Since Pochi allows us to enable MCP per task, we can use Validation MCP to perform the write operations.&lt;/p&gt;

&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;To start with Plan mode we insert the below prompt and click on Plan in the prompt-send option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Create a migration plan to backfill totals &lt;span class="k"&gt;for &lt;/span&gt;orders where total IS NULL. Do not execute anything.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;So far there is no database access involved. You can review the plan, put inline comments and Pochi will modify the plan based on your inputs. &lt;/p&gt;

&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: Generate Migration Script
&lt;/h3&gt;

&lt;p&gt;Once you have the plan finalised, you can generate a migration script. To do so, we prompt back to Pochi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Convert the approved plan into a SQL migration file at: &lt;span class="sb"&gt;`&lt;/span&gt;migrations/backfill_order_totals.sql&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Do not execute anything.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;Pochi generates the following migration file, which can now be reviewed, versioned, and audited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Migration: Backfill order totals from `order_items`&lt;/span&gt;
&lt;span class="c1"&gt;-- Description: Updates `orders.total` where it is `NULL` by summing `price * qty` from `order_items`.&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 1. Identify records to be updated and store in a temporary table for verification/rollback&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;backfill_log&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Perform the backfill&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;backfill_log&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Verification queries (intended to be run before COMMIT)&lt;/span&gt;

&lt;span class="c1"&gt;-- Check if any orders in the log still have NULL totals&lt;/span&gt;
&lt;span class="c1"&gt;-- SELECT COUNT(*) FROM orders WHERE id IN (SELECT id FROM backfill_log) AND total IS NULL;&lt;/span&gt;

&lt;span class="c1"&gt;-- Check for discrepancies between orders.total and sum of order_items&lt;/span&gt;
&lt;span class="c1"&gt;-- SELECT o.id, o.total, SUM(oi.price * oi.qty) as expected&lt;/span&gt;
&lt;span class="c1"&gt;-- FROM orders o&lt;/span&gt;
&lt;span class="c1"&gt;-- JOIN order_items oi ON o.id = oi.order_id&lt;/span&gt;
&lt;span class="c1"&gt;-- WHERE o.id IN (SELECT id FROM backfill_log)&lt;/span&gt;
&lt;span class="c1"&gt;-- GROUP BY o.id, o.total&lt;/span&gt;
&lt;span class="c1"&gt;-- HAVING o.total != SUM(oi.price * oi.qty);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Rollback Strategy:&lt;/span&gt;
&lt;span class="c1"&gt;-- In case of failure before COMMIT, the transaction will roll back automatically.&lt;/span&gt;
&lt;span class="c1"&gt;-- In case of failure after COMMIT (if the temp table is still available in the session):&lt;/span&gt;
&lt;span class="c1"&gt;-- UPDATE orders SET total = NULL WHERE id IN (SELECT id FROM backfill_log);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, still no database values have been modified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Validate in Clone
&lt;/h3&gt;

&lt;p&gt;Now, with only validation MCP enabled, we can ask Pochi to do a write operation by applying the migration to the &lt;code&gt;shop_validate&lt;/code&gt; database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt:  Apply the migration &lt;span class="k"&gt;in &lt;/span&gt;migrations/backfill_order_totals.sql to the validation database and verify that no orders have NULL totals.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;Pochi will read the SQL file, send the SQL to validation MCP service, which will update &lt;code&gt;shop_validate&lt;/code&gt; database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vwvc7sepzd3li6se5yq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vwvc7sepzd3li6se5yq.png" alt="validate-before" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5fx2040yy092k4z84pm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5fx2040yy092k4z84pm.png" alt="validate-after" width="800" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All this time production database &lt;code&gt;shop_sandbox&lt;/code&gt; remains untouched.&lt;/p&gt;

&lt;p&gt;Now if the changes are incorrect or you would like modifications, you can prompt back Pochi. For eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Fix the migration logic and re-run &lt;span class="k"&gt;until &lt;/span&gt;totals are correct. Do not update the migration file yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent will try new SQL and rerun in validation. &lt;/p&gt;

&lt;p&gt;Once you are happy with the changes you can update the SQL file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Prompt: Update migrations/backfill_order_totals.sql to reflect the final validated logic.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8: Deploy to Production (Manual + Approved Pipeline)
&lt;/h3&gt;

&lt;p&gt;Once validation looks good, production updates can be performed manually or via an approved deployment pipeline. In our case, we run the following command to apply the changes from &lt;code&gt;shop_validate&lt;/code&gt; to &lt;code&gt;shop_database&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="n"&gt;apple&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;shop_sandbox&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="n"&gt;migrations&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;backfill_order_totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses the same migration and review process without the agent ever touching the production credentials. You can again verify the details on your production database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkj6ndpeb6ktxbzw2ndzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkj6ndpeb6ktxbzw2ndzo.png" alt="prod-before" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzswr7sc40goubgxh9atj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzswr7sc40goubgxh9atj.png" alt="prod-after" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How this maps to real production setups
&lt;/h2&gt;

&lt;p&gt;In the demo above, we used two local Postgres databases (&lt;code&gt;shop_sandbox&lt;/code&gt; and &lt;code&gt;shop_validate&lt;/code&gt;) to illustrate isolation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6x3djqf0stzhqaa42xpo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6x3djqf0stzhqaa42xpo.png" alt="iwe-system-tutorial" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In real systems, this isolation usually already exists in production, staging, and development database environments. Modern database platforms make it straightforward to create and manage these environments. &lt;/p&gt;

&lt;p&gt;For example, Managed Postgres (RDS, Cloud SQL, Neon, Supabase) has read replicas, cloned databases, and point-in-time snapshots that are restored into new instances. Data warehouses have schema-level clones (Snowflake, BigQuery) and masked production extracts.  &lt;/p&gt;

&lt;p&gt;As a user, all you need to do is route the agent to the correct tier depending on the operation you intend to perform. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we displayed why tool-level restrictions are not enough and how agents can still bypass read-only controls if they have any execution surface.&lt;/p&gt;

&lt;p&gt;The safest approach is to separate reasoning from execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1:&lt;/strong&gt; Read-only access via narrow tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2:&lt;/strong&gt; Writable clones + validated migration scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives agents useful autonomy while keeping your production systems protected.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Give Coding Agents Access to SSH and Databases (Without Breaking Production)</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Sat, 21 Feb 2026 02:19:21 +0000</pubDate>
      <link>https://dev.to/getpochi/how-to-give-coding-agents-access-to-ssh-and-databases-without-breaking-production-3f2e</link>
      <guid>https://dev.to/getpochi/how-to-give-coding-agents-access-to-ssh-and-databases-without-breaking-production-3f2e</guid>
      <description>&lt;p&gt;As AI agents become more capable, teams are trying to limit the damage they can do when given access to SSH or production databases. &lt;br&gt;
Common approaches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Command allowlists: allow &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;; block &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;SQL filters: Allow &lt;code&gt;SELECT&lt;/code&gt;; block &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;DROP&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Manual approval flows: Run everything in &lt;code&gt;read-only&lt;/code&gt; mode until a human explicitly accepts changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F352dm4iobs78mjlkro9k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F352dm4iobs78mjlkro9k.png" alt="cover-image-coding-agents-access" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These practices assume that constraining agent behaviour through rules, filters, and approvals can prevent dangerous actions. &lt;/p&gt;

&lt;p&gt;This assumption is wrong.&lt;/p&gt;

&lt;p&gt;Allowlists, prompts, and approval dialogs are &lt;strong&gt;control surfaces&lt;/strong&gt; that influence what an agent chooses to do. Shells, credentials, runtimes, and database roles function as &lt;strong&gt;execution surfaces&lt;/strong&gt;, defining what the system can do.&lt;/p&gt;

&lt;p&gt;Risk is determined by execution surfaces, not control surfaces. &lt;/p&gt;

&lt;p&gt;Enforcing safety at the behavior layer isn’t the solution. What follows is why these approaches fail in practice - and what actually holds up in production.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why database-level controls fail
&lt;/h2&gt;

&lt;p&gt;Human engineers rarely connect to primary databases with full access. They query replicas or views that cannot change production data. The same principle must apply to agents, but it is not enough to enforce this only at the query level.&lt;/p&gt;

&lt;p&gt;There are several reasons for this.&lt;/p&gt;

&lt;p&gt;First, SQL filtering is unreliable. Even if you block write statements, many databases still support queries that trigger full table scans. Constructs like &lt;code&gt;SELECT INTO&lt;/code&gt; can introduce new tables, and functions can produce side effects.&lt;/p&gt;

&lt;p&gt;Second, read access alone is dangerous. It can expose authentication tokens, PII, or operational metadata. This is why databases themselves do not rely on client-side query validation and instead implement safety through roles, views, and replicas.&lt;/p&gt;

&lt;p&gt;Re-implementing parts of this logic in the agent layer with regexes or heuristics is both fragile and incomplete.&lt;/p&gt;
&lt;h2&gt;
  
  
  Agents route around blocked tools
&lt;/h2&gt;

&lt;p&gt;An agent’s goal is task completion. Blocking individual tools does not mean entire classes of state changes are prevented. For example, if the agent finds that direct deletion within a database schema is blocked, it will reach the same outcome by putting together other allowed operations. &lt;/p&gt;

&lt;p&gt;In this case, even though direct access to the tool is blocked, it doesn't prevent many other possible walkarounds that could apply undesired changes to the production system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0pgm8oym9a1dat8fkrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0pgm8oym9a1dat8fkrs.png" alt="agent-routes" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, this can include writing a script that performs deletion or just invoking a different tool that indirectly gives the expected outcome. This is not adversarial behavior. Instructions, allowlists, and approval dialogs influence behavior, but they do not define what the system is capable of doing. &lt;/p&gt;

&lt;p&gt;For that reason, safety cannot live solely inside the model. It must be enforced outside the model through OS permissions, roles, and tool interfaces. Access control is not a prompting problem. It's an infrastructure problem that will require explicit separation between reasoning and execution, with enforcement applied at deterministic execution boundaries.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually holds up in production
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. &lt;code&gt;Read-only&lt;/code&gt; access still allows irreversible damage
&lt;/h3&gt;

&lt;p&gt;In one setup, we exposed production data to an agent exclusively through a read-only tool interface backed by a &lt;code&gt;SELECT-only&lt;/code&gt; database role. On paper, this appeared safe - the agent could inspect data but had no explicit write tools.&lt;/p&gt;

&lt;p&gt;However, as long as the agent retained access to a general execution surface (shell access, runtime file system, or database credentials), it simply routed around the restriction, generating its own script and updating the database through an unintended path.&lt;/p&gt;

&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;We removed the execution surface entirely and enforced read-only access at the infrastructure level. This involved revoking database write permissions and disabling shell execution. Post this, the agent could reason freely but could no longer apply any changes.&lt;/p&gt;

&lt;p&gt;
        controls&lt;br&gt;
        style={{&lt;br&gt;
        width: "100%",&lt;br&gt;
        borderRadius: "8px",&lt;br&gt;
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",&lt;br&gt;
        }}&lt;br&gt;
    &amp;gt;&lt;br&gt;
        &lt;br&gt;
        Your browser does not support the video tag.&lt;br&gt;
    &lt;/p&gt;

&lt;p&gt;Even when &lt;code&gt;read-only&lt;/code&gt; access is enforced at the infrastructure level, production access still depends on human approval. Over time, these approvals tend to degrade into procedural steps. Auto-approve paths appear, reviews become mechanical, and the safety boundary weakens.&lt;/p&gt;

&lt;p&gt;As a result, granting agents direct access to production databases, even in read-only mode, is best avoided. Agents are non-deterministic by design, and production systems should not be exposed to that uncertainty.&lt;/p&gt;

&lt;p&gt;The challenge is that many legitimate tasks still require writes.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Writes must flow through existing deployment pipelines
&lt;/h3&gt;

&lt;p&gt;Many engineering tasks involve backfills, schema updates, and data correction. All of these require write access.&lt;/p&gt;

&lt;p&gt;Giving an agent write access to production data under these circumstances is rarely acceptable. A more robust pattern is to let agents propose changes, generate and test migration scripts, and iterate freely. But applying those same changes to production will still wait on explicit human approval.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmt0spplaf0xffkbxn10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmt0spplaf0xffkbxn10.png" alt="write-process" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This mirrors standard engineering practice. It avoids giving production credentials to agents and ensures all changes are auditable and reversible.&lt;/p&gt;

&lt;p&gt;The tradeoff is slower iteration, because agents can’t validate assumptions against real production data. This is where isolated writable environments become important.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Isolated writable environments enable safe iteration
&lt;/h3&gt;

&lt;p&gt;Isolated Writable Environments (IWEs) are disposable database instances that mirror production schema and data. Within these environments, agents can evolve schemas, validate queries, and test migrations freely. Once the changes are ready, the same test and migration scripts can be replayed through the original production pipelines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb39akjlcc6cs5sngwnyw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb39akjlcc6cs5sngwnyw.png" alt="iwe-system" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, combining IWEs with gated production deployment yields the best results. You get the agent to perform actions against isolated databases while production remains gated by standard deployment processes.&lt;/p&gt;

&lt;p&gt;The same principle applies outside the database layer. With SSH and shell access, the execution surface becomes effectively unbounded unless similar infrastructure-level boundaries are enforced.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why shells make safety undefined
&lt;/h2&gt;

&lt;p&gt;A shell is a general-purpose programming environment. Once exposed, safety boundaries become undefined.&lt;/p&gt;

&lt;p&gt;An agent can use &lt;code&gt;cat&lt;/code&gt; to overwrite files, using &lt;code&gt;grep&lt;/code&gt; can exfiltrate secrets, and &lt;code&gt;tail -f&lt;/code&gt;, if used on the wrong file, can leak sensitive data indefinitely. On the other hand, having an allowlist to control which binaries are executed does not automatically constrain the kind of operations that are possible on the system.&lt;/p&gt;

&lt;p&gt;Having a shell exposed grants access to the file system, process creation, and the environment state. And once a shell exists, the boundary of what is allowed is no longer clear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8ai0jb253zwfqoy1lz5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8ai0jb253zwfqoy1lz5.png" alt="shell-access" width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The safest designs accept this and shift the focus from preventing mistakes to containing them.&lt;/p&gt;
&lt;h3&gt;
  
  
  Disposable environments
&lt;/h3&gt;

&lt;p&gt;The simplest and most reliable pattern is to treat any machine an agent can access as disposable. This way when something goes wrong, you just replace the whole thing and not spend time to fix it. This is already how CI systems operate, and increasingly how dev environments are provisioned.&lt;/p&gt;

&lt;p&gt;Instead of connecting agents to long-lived servers, teams route them to short-lived containers, ephemeral VMs or dev sandboxes per task or per session. This way agents are free to modify files, install packages and experiment with configurations.  But this introduces infrastructure cost while reducing production impact. &lt;/p&gt;
&lt;h3&gt;
  
  
  Restricted hosts and forced commands
&lt;/h3&gt;

&lt;p&gt;Some teams still require agents for log inspection, operational debugging, or controlled maintenance. But even in these cases, full interactive shells are rarely necessary. Common restrictions can include: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH users with no home directory and limited permissions&lt;/li&gt;
&lt;li&gt;forced commands in &lt;code&gt;authorized_keys&lt;/code&gt; so only specific scripts can run&lt;/li&gt;
&lt;li&gt;wrapper binaries that expose narrow actions instead of general shells&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, instead of allowing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh agent@host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the key may enforce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/local/bin/fetch-logs.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces damage area but comes with its own pitfalls. Debugging can become harder and workflows might require constant tooling updates.&lt;/p&gt;

&lt;p&gt;Structured tools and APIs provide stronger safety guarantees than shells.. A shell exposes the file system, environment variables and network access - making it impossible to create boundaries without building a second operating system around it.&lt;/p&gt;

&lt;p&gt;This approach already mirrors how human access to infrastructure has evolved as well. We now have fewer SSH sessions, more pipelines, dashboards, and automation APIs. Agents benefit from the same shift, for the same reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Throughout this post, we saw the same pattern repeat across different systems. When the agent was instructed to follow safety, through instructions, prompts, or behavioral constraints, it failed. Whereas, when safety was enforced through infrastructure (roles, isolation, and execution boundaries), it held.&lt;/p&gt;

&lt;p&gt;Many teams make this fundamental mistake of treating access control as a prompting problem instead of a system problem.&lt;/p&gt;

&lt;p&gt;The only reliable way to make agents safe is to design environments where destructive actions are physically unreachable, and all state changes flow through auditable, deterministic systems. Once safety is achieved by design, mistakes become recoverable, contained, and reviewable - and only then can agents be given meaningful autonomy across real production use cases.&lt;/p&gt;

</description>
      <category>database</category>
      <category>coding</category>
      <category>software</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We’ve been shipping "slop" for 20 years. We just used to call it an MVP.</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Fri, 09 Jan 2026 15:04:23 +0000</pubDate>
      <link>https://dev.to/getpochi/weve-been-shipping-slop-for-20-years-we-just-used-to-call-it-an-mvp-hn6</link>
      <guid>https://dev.to/getpochi/weve-been-shipping-slop-for-20-years-we-just-used-to-call-it-an-mvp-hn6</guid>
      <description>&lt;p&gt;A lot of people have started using the word “slop” as shorthand for AI-generated code. Their stance is that AI is flooding the industry with low-quality software, and we’re all going to pay for it later in outages, regressions, and technical debt.&lt;/p&gt;

&lt;p&gt;This argument sounds convincing until you look honestly at how software has actually been built for the last 20 years.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that “slop” didn’t start with AI. In fact, it is AI that made it impossible to keep pretending otherwise.&lt;/p&gt;

&lt;p&gt;Let’s pull back the curtain on a silent pact the industry followed, long before the first LLM was trained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Software has always optimized for execution
&lt;/h2&gt;

&lt;p&gt;Outside of Google’s famously rigorous review culture, most Big Tech giants (Meta, Amazon, and Microsoft included) have historically prioritized speed.&lt;/p&gt;

&lt;p&gt;In the real world, PRs are often skimmed, bugs are fixed after users report them, and the architecture itself evolves after the product proves itself. We didn’t call this “slop” back then; we called it an MVP (Minimum Viable Product).&lt;/p&gt;

&lt;p&gt;By comparison, some of the code that coding agents deliver today is already better than the typical early-stage PRs in many companies. AI isn’t introducing a new era of “good enough” code; it’s just the latest tool for a strategy we’ve used for decades.” And in hindsight, we have always been willing to trade internal code purity for external market velocity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Open Source Antidote
&lt;/h2&gt;

&lt;p&gt;The primary exception is open-source projects, which operate differently. Open source has consistently produced reliable, maintainable code, even with contributions from dozens or hundreds of developers.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because open source forces modularity. Unlike internal corporate developers who can reach across a private monolith to create messy dependencies, open-source contributors often work in isolation. To be successful, the project must maintain strict API boundaries and clean abstractions so that someone with zero internal context can contribute without breaking the system.&lt;/p&gt;

&lt;p&gt;This environment creates aggressive iteration loops and context-rich opinions. Every contribution undergoes a series of automated tests and diverse human peer reviews. Unlike internal systems, which remain messy even after years of maintenance, open source libraries receive feedback from diverse sources, which usually converge better on overall quality than code written for one or two specific use cases.&lt;/p&gt;

&lt;p&gt;This trend of prioritizing execution over perfection actually fits most application-layer workflows in companies today. If we treat an AI agent like an external open-source contributor, i.e. someone who needs strict boundaries and automated feedback to be successful, the “slop” disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering Quality into the Agent
&lt;/h2&gt;

&lt;p&gt;At Pochi, we believe the output of an AI agent is only as good as the contextual guardrails you build around it. If you want to avoid”slop”, you have to go further than simple chat prompts. Some tips we found useful were:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Solving the Hallucination Problem&lt;/strong&gt;&lt;br&gt;
The biggest problem with AI code is its tendency to “hallucinate” nonexistent libraries or deprecated syntax. This is because developers convey changes from a “Prompt Engineering” lens instead of an “Environment Engineering” perspective.&lt;/p&gt;

&lt;p&gt;This is solvable if you integrate the agent directly into the CI/CD pipeline, where every line of code can be instantly validated against existing compilers and linters. This way, you don’t have to wait for the AI to get it right, but trust your environment to catch it when it’s wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Utilizing “Cloud Markdown”&lt;/strong&gt;&lt;br&gt;
A “Cloud Markdown” approach is useful for high-scale design practices. Instead of a static PDF with verbose architectural standards, you create a README.pochi.md file that acts as the agent's source of truth.&lt;/p&gt;

&lt;p&gt;An example architectural guardrails file can look 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;#Project Design Patterns 

## Data Fetching
- Rule: No direct fetch calls in components.
- Pattern: Use the useQuery wrapper from @/lib/api.
- Reasoning: Ensures global error handling and caching are applied.
## State Management
- Constraint: All shared state must reside in LiveStore.
- Pattern: const [data, set] = useLiveStore(key);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, you end up with three critical workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentation as Context:&lt;/strong&gt; You can store Markdown files with deep architectural rules and design patterns within the repository.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prompt Injection:&lt;/strong&gt; Before an agent begins a task, it “reads” these Markdown files to understand global restrictions (e.g., “Always use local-first storage patterns via LiveStore”).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context Scaffolding:&lt;/strong&gt; This ensures the agent isn’t just writing a snippet in a vacuum, but is following the specific scaffolding of the existing codebase.&lt;br&gt;
This helps you embed deep architectural knowledge directly into the workflow. Now, before every major migration, the agent gets tasked with gathering as much file-level context as possible to produce the most accurate result.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;At the end of the day, users never see “slop.” They see broken interfaces, slow loading times, crashes, and unreliable features.&lt;/p&gt;

&lt;p&gt;If you dismiss AI code as “slop,” you are missing out on the greatest velocity shift in the history of computing. By combining Open Source discipline (rigorous review and modularity) with AI-assisted execution, we can finally build software that is both fast to ship and resilient to change.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>llm</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>How do you build serious features using only VS Code’s public APIs?</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Fri, 09 Jan 2026 13:46:18 +0000</pubDate>
      <link>https://dev.to/getpochi/how-do-you-build-serious-features-using-only-vs-codes-public-apis-f8k</link>
      <guid>https://dev.to/getpochi/how-do-you-build-serious-features-using-only-vs-codes-public-apis-f8k</guid>
      <description>&lt;p&gt;I've been writing a series on &lt;a href="https://docs.getpochi.com/developer-updates/how-we-created-nes-model/" rel="noopener noreferrer"&gt;how we trained our NES model&lt;/a&gt;, what that &lt;a href="https://docs.getpochi.com/developer-updates/context-management-in-your-editor/" rel="noopener noreferrer"&gt;model takes as context&lt;/a&gt; to make a prediction, and how these model &lt;a href="https://docs.getpochi.com/developer-updates/request-management-in-nes/" rel="noopener noreferrer"&gt;requests are managed with correct timing&lt;/a&gt; under continuous typing.&lt;/p&gt;

&lt;p&gt;With this, we’ve reached a point where NES can predict what edit should happen and when it should appear in the editor.&lt;/p&gt;

&lt;p&gt;Now, we'll talk about how there is still one critical decision to make. &lt;strong&gt;Once a suggestion arrives, how should that change be presented inside a live editor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pochi’s NES is built as a VS Code-native feature, not a standalone IDE or a custom fork. This means previews must integrate with VS Code’s public APIs, performance model, and established interaction patterns.&lt;/p&gt;

&lt;p&gt;This introduces a core design challenge - to surface enough context for a suggestion to be actionable, without disrupting the developer's flow.&lt;/p&gt;

&lt;p&gt;Designing a system that honors this is more than a matter of visual polish; it is a complex systems + UX problem. We’ll explore why this balance is so difficult for a native AI agent and the specific rendering strategies NES uses to achieve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Display Problem
&lt;/h2&gt;

&lt;p&gt;Unlike conventional editor features, NES does not control where the user’s cursor is when a suggestion arrives. The editor is a continuously changing environment and does not function like a static canvas. Sometimes the user's cursor might be exactly where the edit belongs, or it can be twenty lines away, or the suggestion itself can be a huge change spanning multiple lines.&lt;/p&gt;

&lt;p&gt;Showing such suggestions naïvely introduces new failure modes that are easy to trigger and hard to ignore. One experiences jumps in cursor position, abrupt viewport scrolls, or rendering large changes directly in the editing flow. In practice, these behaviors are often more disruptive than not showing a suggestion at all.&lt;/p&gt;

&lt;p&gt;This brings us to the most fundamental design question: &lt;strong&gt;How do we show an edit without stealing the developer’s attention?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Answering that question requires understanding the VS Code interaction model.&lt;/p&gt;

&lt;p&gt;VS Code does not provide a built-in API for previewing LLM-generated edits. Instead, the editor offers different primitives for different kinds of locations and edits. These primitives are optimized for various interaction patterns, each with their own affordances and limitations. Some work well for cursor-local edits, while others are better suited for changes elsewhere in the file.&lt;/p&gt;

&lt;p&gt;Understanding this difference is key. Pochi's NES does not render suggestions in a single, fixed way. Instead, NES relies on these primitives to create a balance between visibility and disruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Rendering Strategy
&lt;/h2&gt;

&lt;p&gt;Rather than forcing all suggestions into a single representation, we designed a Dynamic Rendering Strategy offering the optimal visual experiences in different editing scenarios:&lt;/p&gt;

&lt;p&gt;Suggestions that target the current cursor position are rendered inline, flowing naturally into the user's typing behavior.&lt;br&gt;
Suggestions that apply off-cursor are previewed via an inline diff decoration, avoiding jumps in the viewport.&lt;br&gt;
For large, multi-line block inserts, a floating preview is used to provide sufficient context without disrupting the user's current focus.&lt;br&gt;
This way, each path is deliberately scoped to the situations where it performs best, aligning it with the least disruptive representation for a given edit.&lt;/p&gt;

&lt;p&gt;Let’s take a walk-through of these rendering strategies in detail and examine when each one is used, starting with the least disruptive case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inline Completion
&lt;/h3&gt;

&lt;p&gt;When an edit is positioned right at the cursor, the least disruptive option is to stay out of the way. In such cases, we render the edit inline, making it blend directly into the user's typing flow.&lt;/p&gt;

&lt;p&gt;To achieve this, we use VS Code's inline completion API. This approach works especially well for small, localized changes like autoclosing brackets, replacing a few characters, or edits that are directly made under the cursor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasgy412f4h6cbioy25nx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasgy412f4h6cbioy25nx.gif" alt="Inline Diff" width="760" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Inline Diff Preview
&lt;/h3&gt;

&lt;p&gt;Because NES predicts the next meaningful edit across the file (not just at the cursor), many suggestions naturally apply outside the user’s current editing position. For example, while you are typing inside a function, NES may suggest updating a related import, adjusting a type definition, or fixing a reference several lines away.&lt;/p&gt;

&lt;p&gt;In these cases, the cost of getting the presentation wrong is high. The user is forced to jump across the file, break context and interrupt their flow.&lt;/p&gt;

&lt;p&gt;To avoid that, we render the suggestion as an inline diff decoration. The text to be replaced is highlighted in red, while the new content is shown in green at the insertion point. This way, the user gets a clear preview of the change without moving the cursor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlms2fefaazcmb2nro17.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlms2fefaazcmb2nro17.gif" alt=" " width="600" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This works particularly well for changes involving single-line updates or even multiple lines where each line is being changed independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Floating Diff Image
&lt;/h3&gt;

&lt;p&gt;Because NES has the ability to propose structural edits, such as inserting a new helper function, refactoring a block of logic, or adding a multi-line configuration, it frequently produces multi-line suggestions that cannot be represented as simple, inline changes.&lt;/p&gt;

&lt;p&gt;In these cases, the suggestion is no longer tied to the cursor’s immediate context, and the standard inline rendering stratergies do not suffice.&lt;/p&gt;

&lt;p&gt;At this point, the decision falls under either pulling the user away from where they’re working or bringing the preview to them. Since preserving developer flow is a core design principle for NES, we consistently choose the latter.&lt;/p&gt;

&lt;p&gt;In order to make the suggestion appear near the edit target without moving the cursor, we generate a floating diff preview and render it as an image. The color schema of the suggestion will also stay consistent with the other solutions we discussed previously - red for deleted text, and green for inserted ones.&lt;/p&gt;

&lt;p&gt;VS Code allows extensions to attach image-based decorations. With careful layout and positioning, these decorations can be floated near the edit target and used as a diff preview. However, the editor does not render code into images, which means the preview has to be generated by the extension itself.&lt;/p&gt;

&lt;p&gt;This required a small rendering pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Theme matching:&lt;/strong&gt; Every VS Code theme is an extension with a standard JSON format. We parse the active theme, extract its token colour map, and match it to the user’s active settings so the preview matches the theme in the editor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Syntax highlighting:&lt;/strong&gt; VSCode includes a bundled TextMate runtime. We load the grammar for the current filetype, generate syntax scopes, and apply the same colouring rules that VS Code uses. This ensures that the rendered code maintains the same appearance as the code in the editor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image rendering:&lt;/strong&gt; Here we use canvaskit-wasm to render the tokenized code into an image. To draw the code properly, we took the editor’s current fontSize and lineHeight, drew each tokenized segment at the correct coordinates, then applied diff highlights (additions in green and removals in red). The final image is then surfaced using the decoration API.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8n559te5dx32vbzlepsf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8n559te5dx32vbzlepsf.gif" alt=" " width="720" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach allows multi-line edit suggestions to appear near their target location while preserving cursor position and avoiding viewport jumps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Different kinds of edit suggestions need different presentation strategies, with the editor API playing a decisive role in shaping the final experience.&lt;/p&gt;

&lt;p&gt;Rendering an NES suggestion ended up being less about displaying text and more about maintaining the reader’s attention. Because no matter what, once attention is broken, even the best suggestion gets ignored.&lt;/p&gt;

&lt;p&gt;Each rendering path is designed to stay as close as possible to the developer’s flow while working within the editor’s interaction model.&lt;/p&gt;

&lt;p&gt;At this point in our journey, NES can decide what to suggest (the model), when to surface it (request management), and how to show it without disruption (rendering paths). Combined, these layers define how AI-generated edit results become truly helpful in a real IDE.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>programming</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>How do you build serious features using only VS Code’s public APIs?</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Thu, 08 Jan 2026 15:21:16 +0000</pubDate>
      <link>https://dev.to/getpochi/how-do-you-build-serious-features-using-only-vs-codes-public-apis-3d6b</link>
      <guid>https://dev.to/getpochi/how-do-you-build-serious-features-using-only-vs-codes-public-apis-3d6b</guid>
      <description>&lt;p&gt;I've been writing a series on &lt;a href="https://docs.getpochi.com/developer-updates/how-we-created-nes-model/" rel="noopener noreferrer"&gt;how we trained our NES model&lt;/a&gt;, what that &lt;a href="https://docs.getpochi.com/developer-updates/context-management-in-your-editor/" rel="noopener noreferrer"&gt;model takes as context&lt;/a&gt; to make a prediction, and how these model &lt;a href="https://docs.getpochi.com/developer-updates/request-management-in-nes/" rel="noopener noreferrer"&gt;requests are managed with correct timing&lt;/a&gt; under continuous typing.&lt;/p&gt;

&lt;p&gt;With this, we’ve reached a point where NES can predict what edit should happen and when it should appear in the editor.&lt;/p&gt;

&lt;p&gt;Now, we'll talk about how there is still one critical decision to make. &lt;strong&gt;Once a suggestion arrives, how should that change be presented inside a live editor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pochi’s NES is built as a VS Code-native feature, not a standalone IDE or a custom fork. This means previews must integrate with VS Code’s public APIs, performance model, and established interaction patterns.&lt;/p&gt;

&lt;p&gt;This introduces a core design challenge - to surface enough context for a suggestion to be actionable, without disrupting the developer's flow.&lt;/p&gt;

&lt;p&gt;Designing a system that honors this is more than a matter of visual polish; it is a complex systems + UX problem. We’ll explore why this balance is so difficult for a native AI agent and the specific rendering strategies NES uses to achieve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Display Problem
&lt;/h2&gt;

&lt;p&gt;Unlike conventional editor features, NES does not control where the user’s cursor is when a suggestion arrives. The editor is a continuously changing environment and does not function like a static canvas. Sometimes the user's cursor might be exactly where the edit belongs, or it can be twenty lines away, or the suggestion itself can be a huge change spanning multiple lines.&lt;/p&gt;

&lt;p&gt;Showing such suggestions naïvely introduces new failure modes that are easy to trigger and hard to ignore. One experiences jumps in cursor position, abrupt viewport scrolls, or rendering large changes directly in the editing flow. In practice, these behaviors are often more disruptive than not showing a suggestion at all.&lt;/p&gt;

&lt;p&gt;This brings us to the most fundamental design question: How do we show an edit without stealing the developer’s attention?&lt;/p&gt;

&lt;p&gt;Answering that question requires understanding the VS Code interaction model.&lt;/p&gt;

&lt;p&gt;VS Code does not provide a built-in API for previewing LLM-generated edits. Instead, the editor offers different primitives for different kinds of locations and edits. These primitives are optimized for various interaction patterns, each with their own affordances and limitations. Some work well for cursor-local edits, while others are better suited for changes elsewhere in the file.&lt;/p&gt;

&lt;p&gt;Understanding this difference is key. Pochi's NES does not render suggestions in a single, fixed way. Instead, NES relies on these primitives to create a balance between visibility and disruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Rendering Strategy
&lt;/h2&gt;

&lt;p&gt;Rather than forcing all suggestions into a single representation, we designed a Dynamic Rendering Strategy offering the optimal visual experiences in different editing scenarios:&lt;/p&gt;

&lt;p&gt;Suggestions that target the current cursor position are rendered inline, flowing naturally into the user's typing behavior.&lt;br&gt;
Suggestions that apply off-cursor are previewed via an inline diff decoration, avoiding jumps in the viewport.&lt;br&gt;
For large, multi-line block inserts, a floating preview is used to provide sufficient context without disrupting the user's current focus.&lt;br&gt;
This way, each path is deliberately scoped to the situations where it performs best, aligning it with the least disruptive representation for a given edit.&lt;/p&gt;

&lt;p&gt;Let’s take a walk-through of these rendering strategies in detail and examine when each one is used, starting with the least disruptive case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inline Completion
&lt;/h3&gt;

&lt;p&gt;When an edit is positioned right at the cursor, the least disruptive option is to stay out of the way. In such cases, we render the edit inline, making it blend directly into the user's typing flow.&lt;/p&gt;

&lt;p&gt;To achieve this, we use VS Code's inline completion API. This approach works especially well for small, localized changes like autoclosing brackets, replacing a few characters, or edits that are directly made under the cursor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasgy412f4h6cbioy25nx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasgy412f4h6cbioy25nx.gif" alt="Inline Diff" width="760" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Inline Diff Preview
&lt;/h3&gt;

&lt;p&gt;Because NES predicts the next meaningful edit across the file (not just at the cursor), many suggestions naturally apply outside the user’s current editing position. For example, while you are typing inside a function, NES may suggest updating a related import, adjusting a type definition, or fixing a reference several lines away.&lt;/p&gt;

&lt;p&gt;In these cases, the cost of getting the presentation wrong is high. The user is forced to jump across the file, break context and interrupt their flow.&lt;/p&gt;

&lt;p&gt;To avoid that, we render the suggestion as an inline diff decoration. The text to be replaced is highlighted in red, while the new content is shown in green at the insertion point. This way, the user gets a clear preview of the change without moving the cursor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlms2fefaazcmb2nro17.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlms2fefaazcmb2nro17.gif" alt="Inline diff preview" width="600" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This works particularly well for changes involving single-line updates or even multiple lines where each line is being changed independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Floating Diff Image
&lt;/h3&gt;

&lt;p&gt;Because NES has the ability to propose structural edits, such as inserting a new helper function, refactoring a block of logic, or adding a multi-line configuration, it frequently produces multi-line suggestions that cannot be represented as simple, inline changes.&lt;/p&gt;

&lt;p&gt;In these cases, the suggestion is no longer tied to the cursor’s immediate context, and the standard inline rendering stratergies do not suffice.&lt;/p&gt;

&lt;p&gt;At this point, the decision falls under either pulling the user away from where they’re working or bringing the preview to them. Since preserving developer flow is a core design principle for NES, we consistently choose the latter.&lt;/p&gt;

&lt;p&gt;In order to make the suggestion appear near the edit target without moving the cursor, we generate a floating diff preview and render it as an image. The color schema of the suggestion will also stay consistent with the other solutions we discussed previously - red for deleted text, and green for inserted ones.&lt;/p&gt;

&lt;p&gt;VS Code allows extensions to attach image-based decorations. With careful layout and positioning, these decorations can be floated near the edit target and used as a diff preview. However, the editor does not render code into images, which means the preview has to be generated by the extension itself.&lt;/p&gt;

&lt;p&gt;This required a small rendering pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Theme matching:&lt;/strong&gt; Every VS Code theme is an extension with a standard JSON format. We parse the active theme, extract its token colour map, and match it to the user’s active settings so the preview matches the theme in the editor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Syntax highlighting:&lt;/strong&gt; VSCode includes a bundled &lt;code&gt;TextMate&lt;/code&gt; runtime. We load the grammar for the current filetype, generate syntax scopes, and apply the same colouring rules that VS Code uses. This ensures that the rendered code maintains the same appearance as the code in the editor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image rendering:&lt;/strong&gt; Here we use &lt;code&gt;canvaskit-wasm&lt;/code&gt; to render the tokenized code into an image. To draw the code properly, we took the editor’s current &lt;code&gt;fontSize&lt;/code&gt; and &lt;code&gt;lineHeight&lt;/code&gt;, drew each tokenized segment at the correct coordinates, then applied diff highlights (additions in green and removals in red). The final image is then surfaced using the decoration API.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8n559te5dx32vbzlepsf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8n559te5dx32vbzlepsf.gif" alt="Floating diff preview" width="720" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach allows multi-line edit suggestions to appear near their target location while preserving cursor position and avoiding viewport jumps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Different kinds of edit suggestions need different presentation strategies, with the editor API playing a decisive role in shaping the final experience.&lt;/p&gt;

&lt;p&gt;Rendering an NES suggestion ended up being less about displaying text and more about maintaining the reader’s attention. Because no matter what, once attention is broken, even the best suggestion gets ignored.&lt;/p&gt;

&lt;p&gt;Each rendering path is designed to stay as close as possible to the developer’s flow while working within the editor’s interaction model.&lt;/p&gt;

&lt;p&gt;At this point in our journey, NES can decide what to suggest (the model), when to surface it (request management), and how to show it without disruption (rendering paths). Combined, these layers define how AI-generated edit results become truly helpful in a real IDE.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>programming</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>Everyone says to have better context management. I'll show you how I built it.

https://dev.to/getpochi/nes-series-part-2-real-time-context-management-in-your-code-editor-3jeo</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Tue, 23 Dec 2025 14:53:05 +0000</pubDate>
      <link>https://dev.to/getpochi/everyone-says-to-have-better-context-management-ill-show-you-how-i-built-it-192d</link>
      <guid>https://dev.to/getpochi/everyone-says-to-have-better-context-management-ill-show-you-how-i-built-it-192d</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/getpochi" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3607422%2Fed1a20c6-0f52-43a9-8fc0-079c040296b9.png" alt="getpochi"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/getpochi/nes-series-part-2-real-time-context-management-in-your-code-editor-3jeo" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Everyone says to have better context management. I'll show you how I built it.&lt;/h2&gt;
      &lt;h3&gt;GetPochi ・ Dec 9 '25&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#ai&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#machinelearning&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#llm&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>https://dev.to/getpochi/what-it-really-took-to-train-a-next-edit-suggestion-model-4bf2</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Tue, 23 Dec 2025 14:48:50 +0000</pubDate>
      <link>https://dev.to/getpochi/httpsdevtogetpochiwhat-it-really-took-to-train-a-next-edit-suggestion-model-4bf2-4kh3</link>
      <guid>https://dev.to/getpochi/httpsdevtogetpochiwhat-it-really-took-to-train-a-next-edit-suggestion-model-4bf2-4kh3</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/getpochi" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3607422%2Fed1a20c6-0f52-43a9-8fc0-079c040296b9.png" alt="getpochi"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/getpochi/what-it-really-took-to-train-a-next-edit-suggestion-model-4bf2" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;How I Trained a Next-Edit Suggestion Model for a Coding Agent (32k Github Stars)&lt;/h2&gt;
      &lt;h3&gt;GetPochi ・ Nov 19 '25&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#machinelearning&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#ai&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#opensource&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#architecture&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>How does a code editor decide the right moment to show an LLM-generated code suggestion</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Tue, 23 Dec 2025 14:45:11 +0000</pubDate>
      <link>https://dev.to/getpochi/how-does-a-code-editor-decide-the-right-moment-to-show-an-llm-generated-code-suggestion-2oh7</link>
      <guid>https://dev.to/getpochi/how-does-a-code-editor-decide-the-right-moment-to-show-an-llm-generated-code-suggestion-2oh7</guid>
      <description>&lt;p&gt;In &lt;a href="https://docs.getpochi.com/developer-updates/how-we-created-nes-model/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, we talked about how we trained our NES model to predict the next meaningful edit you’re likely to make in your code.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://docs.getpochi.com/developer-updates/#nes-series-part-2-real-time-context-management-in-your-code-editor" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;, we then covered what the model takes as context to make this edit. This included deep dives into editable regions, user’s edit history, and using the overall project context.&lt;/p&gt;

&lt;p&gt;Together, these two pieces (model + context) form the core intelligence foundation of the NES system. But incorporating them into an end-to-end real-time engineering system requires more thinking about real developer coding behaviour.&lt;/p&gt;

&lt;p&gt;A code editor is a continuously changing space. Developers type, pause, delete, move the cursor, undo, redo, and essentially keep editing, often faster than any model can respond. Even a fast model call involves network latency, debouncing delays, server-side scheduling, and decoding / streaming time.&lt;/p&gt;

&lt;p&gt;If not careful, a request that was correct when it was sent can return a response that arrives a few hundred milliseconds too late. Which means now you end up with edit suggestions for code that no longer exists. This is something that’s termed “haunted” for being technically right but not at the right place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxlo7rab0mitl107bscv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxlo7rab0mitl107bscv.png" alt="Stale response cycle" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means, in practice, a correct edit shown at the wrong moment is perceived as wrong by the user. So even with proper context and a good model, it is equally important to have the correct timing. Then only can the product actually feel useful without being distracting.&lt;/p&gt;

&lt;p&gt;But getting timing right is challenging, due to the ever evolving nature of the user’s editing state. To make NES feel real-time and helpful, we had to reason about what happens before a request is sent, while it’s in-flight, and after the model responds. This is what we call request management.&lt;/p&gt;

&lt;p&gt;Let’s look at it in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The NES Request Management Lifecycle
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4lpragp4bdshswcnd55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4lpragp4bdshswcnd55.png" alt="The NES Request Management Lifecycle" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Request Management of a NES prediction happens in three stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before the request:&lt;/strong&gt; waiting until the user actually pauses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;While the request is in-flight:&lt;/strong&gt; discard anything that becomes outdated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After the model responds:&lt;/strong&gt; keep the suggestion alive if the user continues along the same trajectory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These map to what we technically implement as debouncing, cancellation, and speculative-prediction caching.&lt;/p&gt;

&lt;p&gt;This structure helps bring the intelligent results (what we get with context + model) to users reliably, even as they type continuously. NES continues to run this loop as you type. Let’s take a closer look at how we handle timing at each stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debouncing: Requesting the Model at the Right Moment
&lt;/h3&gt;

&lt;p&gt;The first question we had to tackle was, “When is the right time to send a request?” When a developer is typing continuously, a request on every keystroke has little value and is wasteful. At the same time, waiting too long would make the system feel unresponsive and slow. We had to find that sweet spot that lies in detecting the exact moment the user actually paused typing.&lt;/p&gt;

&lt;p&gt;Most systems solve this with a fixed interval, (say, 100ms), but real-world typing isn’t this predictable. Instead, we decided to adapt the debounce interval based on how the user is behaving right at that moment.&lt;/p&gt;

&lt;p&gt;To achieve this, we made NES pay attention to a handful of lightweight signals.&lt;/p&gt;

&lt;p&gt;For example, typing a &lt;code&gt;.&lt;/code&gt; often means the developer is about to pause to access a method of an object, so we get the signal to shorten the debounce delay. Whereas, if the user is continuously typing through a variable name, we stretch the delay a bit to avoid jumping in too early. And if the model’s recent response times have been slower due to network conditions, we account for that too, so suggestions land at the exact moment the user expects them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fott2vo4o8vtgwf5dvt8j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fott2vo4o8vtgwf5dvt8j.png" alt="Debouncing" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This way, the result is a debounce time window that changes with the user’s rhythm. It is short when the user has paused, and long when they’re in flow, all while making sure it never exceeds 1 second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancellation: Correctness Over Completion
&lt;/h3&gt;

&lt;p&gt;Once a request is finally sent, the editor doesn’t stop moving. A user can continue typing, move the cursor, or undo and redo steps before the model has even started responding. When that happens, the original request becomes stale instantly.&lt;/p&gt;

&lt;p&gt;In such a case, we cancel the original request from the client-side, and in turn, the server propagates the cancellation, with any late responses being discarded without ever getting them rendered to the UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft8nn8tz42fi0ox2pyaeb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft8nn8tz42fi0ox2pyaeb.png" alt="Cancellation" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a deliberate design decision that optimises and enforces correctness in a live, ever-evolving editing system. We would prefer that NES show nothing rather than something misleading.&lt;/p&gt;

&lt;p&gt;If you’re interested in how this works end-to-end, including streaming behaviour, we’ve written more about it here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speculative Prediction: Staying One Step Ahead of the User
&lt;/h3&gt;

&lt;p&gt;Traditional caching is straightforward. If nothing has changed, just reuse the previous response. In the case of NES, this helps to avoid duplicate requests. And to think about it, throwing work away all the time would be expensive if we didn’t balance it out elsewhere.&lt;/p&gt;

&lt;p&gt;But we go a step further. When the model returns an edit suggestion, we don’t just cache it for the exact context that produced it, but also speculate on the next few contexts the user is likely to enter.&lt;/p&gt;

&lt;p&gt;Now, if the user continues typing along the same trajectory, NES doesn't need to call the model again and can continue serving the speculated suggestion. We call this speculative prediction.&lt;/p&gt;

&lt;p&gt;A speculated prediction remains valid as long as the user is essentially still typing into the suggestion and the surrounding context hasn't changed.&lt;/p&gt;

&lt;p&gt;It’ll be better to illustrate this with the help of an example. Suppose a user types:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdlldxo8eu4h4jct2wz4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdlldxo8eu4h4jct2wz4.png" alt="Caching - a" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NES sends a request and gets the following suggestion:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zo2j2tqrxasyt5hzodx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zo2j2tqrxasyt5hzodx.png" alt="Caching - b" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the user then continues to type, resulting in:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8x9c9i6bj579w3280dfq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8x9c9i6bj579w3280dfq.png" alt="Caching - c" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This user edit is part of the received suggestion. Therefore, the suggestion should still be displayed (unless the user has explicitly rejected it by pressing &lt;code&gt;esc&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;By retrieving this result from the forward prediction cache, we can display the suggestion faster and reduce LLM request usage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo60y79rywpohhrel6a8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo60y79rywpohhrel6a8h.png" alt="Caching - d" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, if the user is not satisfied with the cached suggestion, they still have the option to send a new request to get multiple choices. In essence, forward caching helps accelerate the common path, improving the overall experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By the time a suggestion appears in NES, a lot has already happened. Debouncing decides when is the right time to make a request, cancellation makes sure outdated intent never surfaces in the UI, and speculative prediction lets us reuse good existing predictions when the user naturally moves through them.&lt;/p&gt;

&lt;p&gt;While you’d find these techniques are familiar in distributed systems, applying them inside a code editor was a challenge of its own. The primary driving factor wasn’t about throughput or load but about every evolving human intent under motion.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s next?
&lt;/h3&gt;

&lt;p&gt;So far, we’ve focused on how NES decides what to suggest and when those suggestions should appear. With request management in place, we now have a system that ensures LLM-powered edits reach the user only when they’re truly helpful.&lt;/p&gt;

&lt;p&gt;But now that brings us to the next stage of the process: How should these edits be presented?&lt;/p&gt;

&lt;p&gt;NES suggestions aren’t always a single line near the cursor. Sometimes the relevant edit is several lines or even several files away. Presenting enough information for a quick action without breaking the developer’s flow is a surprisingly deep design and engineering challenge.&lt;/p&gt;

&lt;p&gt;This is especially tricky inside a code editor like VS Code, where rendering options are limited. In such cases, how do we preview multi-line edits precisely? How do we make them feel lightweight, immediate, and skimmable, without being modal or disruptive?&lt;/p&gt;

&lt;p&gt;In Part 4, we’ll dive into how we approached these constraints and built a rendering system that enables richer previews and lower-latency interactions for code edits.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Closing the Loop: How Reinforcement Learning is Changing AI Coding</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Sat, 13 Dec 2025 16:13:33 +0000</pubDate>
      <link>https://dev.to/getpochi/closing-the-loop-how-reinforcement-learning-is-changing-ai-coding-4en6</link>
      <guid>https://dev.to/getpochi/closing-the-loop-how-reinforcement-learning-is-changing-ai-coding-4en6</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Using SFT teaches models how to write code, but it is RL that is necessary to teach them what works. On the other hand, introducing RL in software engineering brings its own specific challenges: data availability, signal sparsity, and state tracking. In this post, we’ll break down how recent works address these challenges.&lt;/p&gt;

&lt;p&gt;So far, the focus of RL driven improvements had been based on competitive coding. For example, in LeetCode-style tasks, the model works in a closed loop. It generally receives a clear problem statement and in turn, it generates a single, self-contained solution.&lt;/p&gt;

&lt;p&gt;This means there are no dependencies involved, no files systems to navigate, and no legacy code that can break. It is exactly like solving a logic puzzle in isolation rather than understanding the engineering implications on the overall codebase.&lt;/p&gt;

&lt;p&gt;However, the field of Software Engineering (SWE) in real-world is fundamentally different. It is a stateful, multi-turn interactive problem. A day-to-day involves much more than just writing the correct functions. You often need to navigate a file system, check up on dependency graphs, run the proper tests, and interpret logs in case of errors. This implies, an agent effectively needs to maintain coherence across a long horizon of interactions.&lt;/p&gt;

&lt;p&gt;Which is why RL is an ideal candidate for SWE since agent actions produce verifiable results. At the same time, it also introduces challenges that are not present in single-turn tasks. For example,&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Availability:&lt;/strong&gt; We cannot easily simulate millions of environmental interactions like we can with math problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal Sparsity:&lt;/strong&gt; Often, success signals appear at the very end of a long sequence of edits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State Tracking:&lt;/strong&gt; Along with the static text of the code, the model must understand the dynamic state of the runtime environment&lt;br&gt;
Recent works from Meta and Moonshot AI surfaced how the industry is pivoting from general reasoning RL to domain-specific SWE-RL to address these challenges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Problem
&lt;/h2&gt;

&lt;p&gt;In order to learn trial and error, the standard RL requires an agent to interact with an environment. For coding, this means running a test suite or compiling code. As compared to verifying math proof, this simulation will be prohibitively slow and expensive in a real-world setting. Here, the engineering challenges arises to figure out how to bootstrap the entire learning process without being dependent on costly online simulation.&lt;/p&gt;

&lt;p&gt;Meta proved that you can bypass the online bottleneck by using the massive offline history of Github. In its recent work on &lt;strong&gt;SWE-RL&lt;/strong&gt; they talked through this approach instead of setting up a live sandbox for every training step.&lt;/p&gt;

&lt;p&gt;But offline data lacks a reward signal. For every historical Pull Request, you cannot easily go back in time and execute tests. SWE-RL solves this by creating a process proxy reward. They calculate the fine grained text similarity between the generated patch and the actual developer ground truth solution instead of just checking if the code runs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapuh470tbrzetiymdmfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapuh470tbrzetiymdmfs.png" alt="Process Proxy Reward" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Depending on how closely the generated format matches the human solution, the model receives a continuous reward. On the other hand, if the model generates an invalid patch format, it receives a penalty. This demonstrates that even before touching a compiler, you can teach a model complex engineering behaviours like navigating file structures and adhering to project conventions using static history.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Signal Sparsity Problem
&lt;/h2&gt;

&lt;p&gt;Next, we have the credit assignment problem while facing online training within executable environments. That means it is difficult to indentify which step really contributed to the final success of the model and which step should get the reward. This reflects on software engineering as any agent can fail after 50 steps of editing and testing. Standard RL struggles to identify which specific step caused the failure.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Kimi-Dev&lt;/strong&gt; paper addresses this through task decomposition. It treats software engineering as a composition of atomic skills: the BugFixer (editing logic) and the TestWriter (verifying logic) instead of training an end-to-end agent to solve the issue immediately.&lt;/p&gt;

&lt;p&gt;Their solution starts with Agentless RL. They train the model specifically on these short horizon atomic tasks using outcome based rewards. They look for signals on whether the patch passed the test or did the test reproduce the bug. And since the tasks are scoped down, the feedback signal becomes dense and clear.&lt;/p&gt;

&lt;p&gt;Kimi-Dev shows that with minimal additional data, a model having these pre-trained capabilities can be adapted to a complex multi-turn agent framework. This suggests that the most efficient path to autonomous agents is rigorous skill acquisition’s followed by workflow adaptation rather than brute force end to end training.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State Problem: Building a Code World Model
&lt;/h2&gt;

&lt;p&gt;Coming to the final challenge, which is also arguably the most profound. Engineers generally do not just read code as text but also think about the execution loops in their mind. This involves tracking how variables change in memory and how functions interact between files. Meanwhile, since current code LLMs lack this internal compiler engine, they just merely predict the next token based on statistical likelihood.&lt;/p&gt;

&lt;p&gt;Meta &lt;strong&gt;Code World Model&lt;/strong&gt; addresses this by fundamentally changing the training curriculum. They realized that waiting until the RL phase to teach execution dynamics is too late. The rewards are too sparse and the gradient vanishes on hard problems.&lt;/p&gt;

&lt;p&gt;Instead, in the mid-training stage, they inject process supervision directly. To teach the physics of code, they constructed two massive datasets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python Execution Traces:&lt;/strong&gt; With over 120 million examples, the model is trained to predict not just the next line of code, but also the exact state of runtime variables (the values in memory) after every single line.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ForagerAgent Trajectories:&lt;/strong&gt; Agents with 3 million trajectories that interact with a Docker environment to solve tasks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This forces the model to internalise a Code World Model. By the time the model enters the final RL stage it is no longer starting from scratch. It already understands if I write X then variable Y changes to Z.&lt;/p&gt;

&lt;p&gt;Consequently, the RL stage becomes a process of Goal Alignment. It uses sparse result rewards like passing tests simply to guide a model. It already understands execution physics to select the specific path that satisfies the verification requirement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4u4ceytci5ygp2f986q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4u4ceytci5ygp2f986q.png" alt="Goal Alignment" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway: Moving Toward Verifiable Agents
&lt;/h2&gt;

&lt;p&gt;This progression from &lt;strong&gt;SWE-RL (offline proxy rewards)&lt;/strong&gt; to &lt;strong&gt;Kimi-Dev (decomposed skill learning)&lt;/strong&gt; and &lt;strong&gt;CWM (execution-trace world models)&lt;/strong&gt; outlines a clear engineering roadmap for the next generation of code models and agentic RL frameworks.&lt;/p&gt;

&lt;p&gt;We are seeing a shift from generic reasoning to specialized engineering. Future models will be more than just smart. They will be grounded in repository history, capable of self-verification through test writing, and possess an explicit internal model of runtime state.&lt;/p&gt;

&lt;p&gt;At &lt;a href="//www.tabbyml.com"&gt;TabbyML&lt;/a&gt; we view these developments as the foundation for Verifiable Engineering. The future value of AI in software development lies in building agents that understand and respect the state of your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Papers:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://arxiv.org/abs/2502.18449" rel="noopener noreferrer"&gt;SWE-RL: Advancing LLM Reasoning via Reinforcement Learning on Open Software Evolution&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://arxiv.org/abs/2509.23045" rel="noopener noreferrer"&gt;Kimi-Dev: Agentless Training as Skill Prior for SWE-Agents&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://arxiv.org/abs/2510.02387" rel="noopener noreferrer"&gt;CWM: An Open-Weights LLM for Research on Code Generation with World Models&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Everyone says to have better context management. I'll show you how I built it.</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Tue, 09 Dec 2025 08:36:13 +0000</pubDate>
      <link>https://dev.to/getpochi/nes-series-part-2-real-time-context-management-in-your-code-editor-3jeo</link>
      <guid>https://dev.to/getpochi/nes-series-part-2-real-time-context-management-in-your-code-editor-3jeo</guid>
      <description>&lt;p&gt;In &lt;a href="https://docs.getpochi.com/developer-updates/how-we-created-nes-model/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, we covered how we trained our NES model, including topics such as the special tokens we use, the LoRA-based fine-tuning on Gemini Flash Lite, and how we utilized a judge LLM to evaluate the model.&lt;/p&gt;

&lt;p&gt;However, the end experience is far more than just building a good model. To make NES feel “intent-aware” inside your editor, we needed to give the model the right context at the right moment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj6dr278xpexf7u29aezb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj6dr278xpexf7u29aezb.png" alt="NES cover image" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In part 2, we’ll talk about that runtime system, or to be precise, how Pochi manages, ranks, and streams real-time edit context. This is the core that helps NES to understand your intent and predict the next meaningful change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Context Management Matters
&lt;/h2&gt;

&lt;p&gt;To start, let’s understand what context management is. In our case, it’s the layer between when a user starts typing and when the model is called with a well-formed prompt. During that in-between phase, the system gathers and prepares all the relevant context the LLM needs before we make a model request.&lt;/p&gt;

&lt;p&gt;As to why it matters, imagine simply sending the entire file to the model on every keystroke. Not only will the model become slower and noisier, but you’d get unstable predictions and over 20 model calls per second, rendering the whole experience unusable.&lt;/p&gt;

&lt;p&gt;Instead, as previewed in the first article, we provide NES with three kinds of context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File Context:&lt;/strong&gt; text, filepath, cursor position, and the region to be edited&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit History:&lt;/strong&gt; record of recent edit steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional context from other files (optional):&lt;/strong&gt; e.g., functions/type declarations that help understand the current file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these depends on clever filtering, segmentation, and timing - all of which happen in milliseconds during normal typing, as we’ll learn below.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. File Context: Finding the “live” region of code
&lt;/h2&gt;

&lt;p&gt;The first question to solve: “Where is the user editing right now?”. This is the foundation of every NES prompt. We answer this by gathering three quick pieces of information from the VS Code API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The current file text&lt;/li&gt;
&lt;li&gt;The file path&lt;/li&gt;
&lt;li&gt;The user’s cursor position &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using this information, we compute what is called “the editable region”. This region is generally a small code window around the user’s cursor of ~10 lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why ~10 lines?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because realistically, the next edit will almost always happen very close to where the user is already editing. This small window keeps the latency extremely low and is large enough to capture the structure around the edit.&lt;/p&gt;

&lt;p&gt;And while we observe many models are over-eager and hallucinate changes elsewhere, our model is prevented from rewriting parts of the file the user wasn’t touching.&lt;/p&gt;

&lt;p&gt;An example of the editable region would be:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonampzpvruzsu3k5mei6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonampzpvruzsu3k5mei6.png" alt="File context" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Edit history: Following the user’s intent over time
&lt;/h2&gt;

&lt;p&gt;So far, we have learnt where the user is currently editing, but we also need to understand how the code is changing over time. This is the part where edit history becomes important for the edit model to predict the user’s intent.&lt;/p&gt;

&lt;p&gt;Now, while we could use the VS Code API to register a listener for text change events, this ends up triggering an event for almost every keystroke. For example, if a user updates a type from string to email, it ends up producing ~6 events.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rt4l2y3surszicdoqrx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rt4l2y3surszicdoqrx.png" alt="Edit history" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are not your meaningful edit steps. If we send this to the model, it will think each keystroke is a new “user intent” and will fire too many requests with wildly different predictions. Instead, we reconstruct real edit steps using an internal change segmentation grouping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How we group events into meaningful steps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since we cannot directly use the listener events, we decided to reduce them to events that represent edit steps. To achieve this, we group raw text-change events into undo-redo scale units.&lt;/p&gt;

&lt;p&gt;Most editors record undo-redo steps on a word scale - for example, when a user inputs a sentence, an undo action will revert the last input word. In our case, for building edit prediction prompts, we do this on a larger scale.&lt;/p&gt;

&lt;p&gt;Once we receive information on a user’s cursor position and tracking gets initiated, we create an edit steps list, where each step is an accumulation of several text change events. We found that 5 steps is the sweet spot to build a prompt. Anything more than that adds noise, and if less, loses the intent.&lt;/p&gt;

&lt;p&gt;For each received text change event, we check if it is adjacent to the previous one. If yes, it belongs to the same edit step; otherwise, if it happens in a different part of the file, we consider it as a new edit step. &lt;/p&gt;

&lt;p&gt;So continuing our example from earlier, if the user happens to add a validateEmail function next, we now have two edit steps in tracking.&lt;br&gt;
The first edit step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyargaf5ws78asbg67lrj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyargaf5ws78asbg67lrj.png" alt="First step" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second edit step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn7zu3ofr2xte5lfchy4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn7zu3ofr2xte5lfchy4q.png" alt="Second Step" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NES receives these steps wrapped inside &amp;lt;|edit_history|&amp;gt; token to learn how the code is evolving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Special Case: Git Checkout Noise&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One edge case we uncovered is when users run git checkout to switch branches. This triggers massive file changes, none of which represent real user intent. If we were to treat these as edit steps, the model would end up thinking the user rewrote half the codebase. In order to avoid polluting the model direction, we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor the git status&lt;/li&gt;
&lt;li&gt;Reset edit history when it changes (checkout, pull, stash)&lt;/li&gt;
&lt;li&gt;Resume tracking after a few seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Additional Context: Bringing in the rest of your project
&lt;/h2&gt;

&lt;p&gt;Code rarely exists in isolation. If you’re editing a function call, the model may need the definition. Likewise, if you’re modifying a type, the model may need the type declaration.&lt;/p&gt;

&lt;p&gt;To give NES this kind of project-aware understanding, we pull additional snippets using the user’s installed language server. For this, we have two VS Code / LSP APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We use &lt;code&gt;vscode.provideDocumentRangeSemanticTokens&lt;/code&gt; to scan the editable region for each token type. Then we can find the tokens of interest, like a function, interface, or type defined in another file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, we use the VS Code command&lt;code&gt;vscode.executeDefinitionProvider&lt;/code&gt; to get the target location for the definition code snippets. This is like Ctrl / Cmd + clicking on a function to see the definition in another file.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two commands are provided by the language server (LSP), which should be available when the language plugin is installed in VS Code. We then extract the definition snippet and include it in &lt;code&gt;&amp;lt;|additional_context|&amp;gt;&lt;/code&gt; token as shown below:NES Cover Image&lt;/p&gt;

&lt;p&gt;This gives the model the same context a developer would mentally reference before typing the next edit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; We do realise that some of the functions could be huge or a type might be hundreds of lines, with LSP sometimes returning entire class bodies. Therefore, to throttle/limit semantic snippet extraction, we’ve currently hard-coded a maximum of 2000 characters per snippet for now.&lt;/p&gt;

&lt;p&gt;Meanwhile, in cases where good LSP support is lacking, like plain text, we don’t add any related snippets context to the prompt. Instead, the prompt will still contain the prefix, suffix, and edit records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;As learned above, every NES request contains the &lt;code&gt;&amp;lt;|editable_region|&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;|edit_history|&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;|additional_context|&amp;gt;&lt;/code&gt;tokens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzf7wzmnqrirrfhwlmh21.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzf7wzmnqrirrfhwlmh21.png" alt="Additional context" width="800" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the end, each piece is carefully constructed into the model exactly the way it was trained. This symmetry between training and runtime makes NES far more reliable than native autocomplete-style approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next?
&lt;/h2&gt;

&lt;p&gt;In our next post, we’ll talk about Request Management, the system that ensures the model never gets a chance to be wrong about the user’s current context.&lt;/p&gt;

&lt;p&gt;We all understand real coding experience involves a lot of short, focused typing, moving the cursor to different places, and continuing to edit while a request is still in flight. This means the model requests can become outdated before their response arrives, or worse, it might produce suggestions for code that no longer exists.&lt;/p&gt;

&lt;p&gt;One of the reasons NES feels fast is because everything that isn’t the latest user intent is thrown away immediately. This cancellation of stale predictions is one of the biggest reasons Pochi’s NES feels so smooth and accurate.&lt;/p&gt;

&lt;p&gt;More on this in our upcoming Part 3 post. Stay tuned!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>How We Built True Parallel Agents With Git Worktrees</title>
      <dc:creator>GetPochi</dc:creator>
      <pubDate>Mon, 24 Nov 2025 05:57:18 +0000</pubDate>
      <link>https://dev.to/getpochi/how-we-built-true-parallel-agents-with-git-worktrees-2580</link>
      <guid>https://dev.to/getpochi/how-we-built-true-parallel-agents-with-git-worktrees-2580</guid>
      <description>&lt;h2&gt;
  
  
  Background Context
&lt;/h2&gt;

&lt;p&gt;We’re building &lt;a&gt;Pochi&lt;/a&gt;, a full-stack AI teammate that can handle all your coding tasks and think, communicate, and work like a real engineer. One of our recent feature requests involved releasing Parallel agents. &lt;/p&gt;

&lt;p&gt;Most teams rarely work on a single task at a time. You might be partway through a feature when a bug report arrives, someone needs a small refactor reviewed, or a documentation fix is pending.&lt;/p&gt;

&lt;p&gt;So you end up switching branches, stashing and popping changes, resetting your workspace, and trying to hold the original task in your head. This is context switching, and it’s one of the biggest hidden costs in software development.&lt;/p&gt;

&lt;p&gt;Parallel Agents were introduced to remove this cost. They are not new, but the way most tools implement them still felt off. Our own experience with Cursor / Github Copilot and the likes showcased that these tools operate as parallel agents inside a single editor tab. So in essence, you’re effectively still working in one tab at a time: switching tasks means switching the state of the same working directory and the same conversation.&lt;/p&gt;

&lt;p&gt;This is the part that matters. When the underlying repo state is shared, “parallel tasks” are still serial in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design Question We Asked
&lt;/h2&gt;

&lt;p&gt;What if multiple agents could work on the same codebase without sharing the same working directory?&lt;/p&gt;

&lt;p&gt;The answer already exists in Git: git worktree add path branch. A worktree gives you a second checked-out working directory backed by the same .git repository.  &lt;/p&gt;

&lt;p&gt;So instead of trying to simulate “parallel tasks” in one tab, if we made each agent correspond to its own worktree, we could expose these worktrees directly inside VS Code (Source Control + Pochi tabs). That means no manual git worktree management is required as each agent simply gets its own branch, filesystem, and local execution context.&lt;/p&gt;

&lt;p&gt;Parallel agents only feel parallel when the filesystem is parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;p&gt;Based on that, we built Parallel Agents in Pochi that use separate Git worktrees, so that each task has its own working directory, branch, chat history, and terminal environment. This means that each task state stays isolated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feydhdqwnc4lyc2je85mc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feydhdqwnc4lyc2je85mc.png" alt=" " width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A great example would be to run the same task with different models to pick the best response. Won’t that be a faster and much better experience - all within the same timeframe?&lt;/p&gt;

&lt;p&gt;From a UX standpoint, the important bit is how it surfaces in the editor: each agent is a separate tab, each tab bound to its own worktree. You can diff, commit, discard, or merge worktrees independently. You can run two model-generated solutions side-by-side and compare outcomes without the branches stepping on each other.&lt;/p&gt;

&lt;p&gt;Imagine if you’ve to run the same task with different models to pick the best response. Won’t a multi tab approach be a faster and much better experience - all within the same timeframe? &lt;/p&gt;

&lt;p&gt;While under the hood it’s git worktrees with orchestration that binds each worktree to its own agent state. &lt;/p&gt;

&lt;h2&gt;
  
  
  How to use?
&lt;/h2&gt;

&lt;p&gt;You can create a worktree from the Pochi sidebar or from the Source Control panel in VS Code. Once a worktree exists, starting a task in that worktree opens it as its own tab in Pochi. &lt;/p&gt;

&lt;p&gt;You can switch tabs to switch tasks. Each tab reflects a complete development context: code, chat, history, and tooling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr0ulaxjzf5yszbu044ym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr0ulaxjzf5yszbu044ym.png" alt=" " width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a task is complete, you can view a diff of that worktree against the main branch and create a PR. Or you can discard it entirely. &lt;br&gt;
The point is that the work is isolated, so it doesn’t interfere with anything that is in progress. Additionally, you can also open an integrated terminal directly inside each task’s worktree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7bf99sozizls6s4bvqvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7bf99sozizls6s4bvqvb.png" alt=" " width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use Parallel Agents
&lt;/h2&gt;

&lt;p&gt;Parallel Agents are most useful when you want to avoid breaking focus on ongoing work: quick bugfixes during feature development, long-running refactors that you want to keep separate, documentation changes that happen alongside coding, or letting an AI assistant explore broader changes in a sandbox. &lt;/p&gt;

&lt;p&gt;On the other hand, if a change is meant to be reviewed and merged as a single unit, keeping it on one branch remains simpler.&lt;/p&gt;

&lt;p&gt;You can refer the full documentation here: &lt;a href="https://docs.getpochi.com/parallel-agents/" rel="noopener noreferrer"&gt;https://docs.getpochi.com/parallel-agents/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In case you’d like to give Pochi a try, you can install the extension &lt;a href="https://marketplace.visualstudio.com/items?itemName=TabbyML.pochi" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

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