<?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: Vishnu JP</title>
    <description>The latest articles on DEV Community by Vishnu JP (@vishnujp).</description>
    <link>https://dev.to/vishnujp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4129744%2Fc16693ed-11c3-496b-8792-8a7094309a0f.jpg</url>
      <title>DEV Community: Vishnu JP</title>
      <link>https://dev.to/vishnujp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vishnujp"/>
    <language>en</language>
    <item>
      <title>Image tracking vs 3D object recognition in AR: when a printed marker isn't enough</title>
      <dc:creator>Vishnu JP</dc:creator>
      <pubDate>Mon, 09 Nov 2026 10:35:46 +0000</pubDate>
      <link>https://dev.to/iboson/image-tracking-vs-3d-object-recognition-in-ar-when-a-printed-marker-isnt-enough-6h1</link>
      <guid>https://dev.to/iboson/image-tracking-vs-3d-object-recognition-in-ar-when-a-printed-marker-isnt-enough-6h1</guid>
      <description>&lt;p&gt;Most AR projects start with image tracking. It's built into ARKit and ARCore, it's well documented, and you can have content floating above a poster in an afternoon.&lt;/p&gt;

&lt;p&gt;Then someone asks: &lt;em&gt;"Can it work on the machine itself, not on a sticker we put on the machine?"&lt;/em&gt; And the project gets a lot harder.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://noxvision.ai" rel="noopener noreferrer"&gt;NoxVision&lt;/a&gt;, an SDK for recognising real 3D objects in AR apps, so we see this moment a lot. This post covers how image tracking works, the five situations where it breaks, and how to decide whether you actually need 3D object recognition. Most of it applies whatever SDK you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image tracking in 20 lines
&lt;/h2&gt;

&lt;p&gt;In Unity with AR Foundation, image tracking needs three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;reference image library&lt;/strong&gt; (&lt;code&gt;XRReferenceImageLibrary&lt;/code&gt;) with your images and their &lt;strong&gt;real-world size&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;ARTrackedImageManager&lt;/code&gt; on your XR Origin, pointing at that library&lt;/li&gt;
&lt;li&gt;A script that reacts when an image is found&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a minimal script that places content on a poster and hides it when the poster goes out of view (AR Foundation 5.x):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.XR.ARFoundation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.XR.ARSubsystems&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RequireComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARTrackedImageManager&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PosterContent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;contentPrefab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;ARTrackedImageManager&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ARTrackedImageManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnEnable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trackedImagesChanged&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;OnChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDisable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trackedImagesChanged&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;OnChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARTrackedImagesChangedEventArgs&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;added&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="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;referenceImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"SpringPoster"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contentPrefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trackingState&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;TrackingState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tracking&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visible&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;In AR Foundation 6, &lt;code&gt;trackedImagesChanged&lt;/code&gt; is replaced by the &lt;code&gt;trackablesChanged&lt;/code&gt; event, but the idea is the same.&lt;/p&gt;

&lt;p&gt;The content is a child of the tracked image's transform, so it moves with the poster. Simple, fast and reliable, as long as you're tracking the right kind of thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick check before you commit to an image
&lt;/h2&gt;

&lt;p&gt;Not every image tracks well. Tracking works by finding distinctive feature points, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photos and busy patterns&lt;/strong&gt; track well&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain colours, simple logos, text on white and repeating patterns&lt;/strong&gt; track badly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google's &lt;code&gt;arcoreimg&lt;/code&gt; command-line tool scores a candidate image from 0 to 100, and Google recommends a score of at least 75. Run it before you print 5,000 flyers.&lt;/p&gt;

&lt;p&gt;Also set the &lt;strong&gt;physical size&lt;/strong&gt; in the reference library accurately. ARKit relies on it to work out how far away the image is, and a wrong size puts your content in the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five situations where image tracking breaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The thing isn't flat
&lt;/h3&gt;

&lt;p&gt;Image tracking assumes a flat, rigid surface. A label wrapped around a bottle, a curved dashboard or a fabric bag bends the image, and tracking becomes unstable or fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. People approach from the side
&lt;/h3&gt;

&lt;p&gt;Tracking works best when the camera faces the image. At steep angles, the features are squashed and the tracker loses them. A poster on a wall is fine. A part inside an engine bay, seen from above and the side, is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The real object has no printed surface
&lt;/h3&gt;

&lt;p&gt;Machine parts, tools, furniture, sculptures and most products on a shelf have no flat, high-detail image on them. You can stick a marker on, but now your app depends on the sticker being there, clean and undamaged.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The same object looks different from different sides
&lt;/h3&gt;

&lt;p&gt;A shoe from the front and from the side are two different images. You can add both to the library, but you're now maintaining many images per object, and you still have gaps between the angles.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. You need to know &lt;em&gt;which&lt;/em&gt; object, not just &lt;em&gt;that&lt;/em&gt; there's an image
&lt;/h3&gt;

&lt;p&gt;If you're identifying one of 50 similar machine parts, image tracking only helps if each one has its own marker. The question you actually want answered is "which part is this?", and that's object recognition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options for real 3D objects
&lt;/h2&gt;

&lt;p&gt;When image tracking isn't enough, teams usually end up with one of three approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform-specific object detection.&lt;/strong&gt; ARKit can detect real objects on iOS using a scanned reference object. It works, but it's Apple-only, so your Android and Unity-on-Android users need something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Train your own model.&lt;/strong&gt; Collect hundreds or thousands of photos of each object from different angles and in different light, label them, train a detection model, and repeat for every new object. Powerful, but it needs ML skills and weeks of work per batch of objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a 3D model of the object as the reference.&lt;/strong&gt; If you have a 3D model, from a scan or from CAD, the model already describes what the object looks like from every angle. This is the approach we took.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to decide
&lt;/h2&gt;

&lt;p&gt;Answer these before writing code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;If yes&lt;/th&gt;
&lt;th&gt;If no&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is the target flat and printed?&lt;/td&gt;
&lt;td&gt;Image tracking&lt;/td&gt;
&lt;td&gt;Keep going&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you control the design of the target?&lt;/td&gt;
&lt;td&gt;Image tracking, designed to track well&lt;/td&gt;
&lt;td&gt;Keep going&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Will people see it from many angles?&lt;/td&gt;
&lt;td&gt;3D recognition&lt;/td&gt;
&lt;td&gt;Image tracking may be enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you need to tell similar objects apart?&lt;/td&gt;
&lt;td&gt;3D recognition&lt;/td&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you need iOS &lt;strong&gt;and&lt;/strong&gt; Android?&lt;/td&gt;
&lt;td&gt;Avoid single-platform options&lt;/td&gt;
&lt;td&gt;Platform-native is fine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A rule of thumb: if you're about to stick a printed marker on a real object so your app can find it, you probably need 3D recognition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where NoxVision fits
&lt;/h2&gt;

&lt;p&gt;NoxVision is built for the "use a 3D model as the reference" approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scan&lt;/strong&gt; the real object with an app like Polycam or Luma AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; the model as &lt;code&gt;.glb&lt;/code&gt;, &lt;code&gt;.ply&lt;/code&gt; or &lt;code&gt;.obj&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect&lt;/strong&gt; it through the phone camera in real time, using the SDK for Android, iOS or Unity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's no custom model training, and detection can start within minutes of uploading. SDK calls use a personal API key from your dashboard's Developer Settings; models are private to your account, and there's offline model caching for places with poor connectivity, such as factory floors.&lt;/p&gt;

&lt;p&gt;A good way to test the idea: pick one object you know well, scan it, and see how detection holds up from different angles and in different light. That tells you more about your use case than any demo.&lt;/p&gt;

&lt;p&gt;And if your target is a flat poster, use image tracking. It's simpler, and it's built in.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What are you tracking?&lt;/strong&gt; If you've built AR, have you hit a case where image tracking wasn't enough? What did you switch to? Tell us in the comments.&lt;/p&gt;

</description>
      <category>augmentedreality</category>
      <category>unity3d</category>
      <category>computervision</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Letting an AI agent query your production database? Do these 6 things first</title>
      <dc:creator>Vishnu JP</dc:creator>
      <pubDate>Mon, 02 Nov 2026 09:51:54 +0000</pubDate>
      <link>https://dev.to/iboson/letting-an-ai-agent-query-your-production-database-do-these-6-things-first-4hj5</link>
      <guid>https://dev.to/iboson/letting-an-ai-agent-query-your-production-database-do-these-6-things-first-4hj5</guid>
      <description>&lt;p&gt;"Just ask the database in plain English" is one of the most useful things an AI agent can do. A sales lead types &lt;em&gt;"Which 10 customers spent the most this quarter?"&lt;/em&gt; and gets an answer in seconds, instead of waiting two days for an analyst.&lt;/p&gt;

&lt;p&gt;It's also one of the scariest things to set up. You're giving software that writes its own SQL a connection to your data.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://aiagents.dev" rel="noopener noreferrer"&gt;AI Agent Studio&lt;/a&gt;, a no-code platform where agents can answer questions from a live database, so we think about this a lot. Here's the setup we recommend before connecting &lt;strong&gt;any&lt;/strong&gt; AI tool to a database, ours included.&lt;/p&gt;

&lt;p&gt;The examples use PostgreSQL, and we ran every snippet on PostgreSQL 16 before publishing. The ideas carry over to MySQL, SQL Server and the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What it protects against&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. A dedicated read-only role&lt;/td&gt;
&lt;td&gt;The agent changing or deleting data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Views instead of raw tables&lt;/td&gt;
&lt;td&gt;The agent reading personal data it doesn't need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Timeouts&lt;/td&gt;
&lt;td&gt;One bad query tying up the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Row limits&lt;/td&gt;
&lt;td&gt;Huge result sets and multi-statement tricks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. A read replica&lt;/td&gt;
&lt;td&gt;Analytics slowing down your app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Logging&lt;/td&gt;
&lt;td&gt;Wrong answers you can't explain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Create a dedicated read-only role
&lt;/h2&gt;

&lt;p&gt;Never reuse your app's database user. Give the agent its own role that can only read, and only from one schema:&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;-- A separate schema for everything the agent may see (more in step 2)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;reporting&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;ai_agent&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;'use-a-long-random-password'&lt;/span&gt; &lt;span class="k"&gt;CONNECTION&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&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&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_agent&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="n"&gt;reporting&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_agent&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="n"&gt;reporting&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Make views and tables created later in this schema readable too&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="n"&gt;reporting&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="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Extra safety: every transaction starts read-only&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;ai_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;default_transaction_read_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SELECT&lt;/code&gt;-only grants are the real protection. &lt;code&gt;default_transaction_read_only&lt;/code&gt; is a second layer, but a session can switch it off with one &lt;code&gt;SET&lt;/code&gt;, so don't rely on it alone. &lt;code&gt;CONNECTION LIMIT&lt;/code&gt; stops a runaway agent from opening hundreds of connections.&lt;/p&gt;

&lt;p&gt;Two details that catch people out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ALTER DEFAULT PRIVILEGES&lt;/code&gt; only applies to objects created &lt;strong&gt;by the role that ran it&lt;/strong&gt;. Run it as the same user that will create your views.&lt;/li&gt;
&lt;li&gt;Store the password in your secrets manager, not in the agent's prompt or config file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Expose views, not raw tables
&lt;/h2&gt;

&lt;p&gt;The role above can only see the &lt;code&gt;reporting&lt;/code&gt; schema. Put &lt;strong&gt;views&lt;/strong&gt; there that contain only what the agent needs, and leave out personal 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="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;reporting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_summary&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;o&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;o&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="n"&gt;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;            &lt;span class="c1"&gt;-- no names, emails or addresses&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default a view runs with its owner's privileges, so &lt;code&gt;ai_agent&lt;/code&gt; can read &lt;code&gt;reporting.orders_summary&lt;/code&gt; without any access to &lt;code&gt;public.orders&lt;/code&gt; or &lt;code&gt;public.customers&lt;/code&gt;. (If you create views with &lt;code&gt;security_invoker = true&lt;/code&gt;, that changes, and the agent would need access to the underlying tables.)&lt;/p&gt;

&lt;p&gt;Then describe the data. Many AI tools read the database's comments when they work out what to query, and plain-English descriptions make their SQL much more accurate:&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;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;reporting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_summary&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'One row per order. total_amount is in USD, including tax.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;reporting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'pending, paid, refunded or cancelled'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clear names help just as much. &lt;code&gt;total_amount_usd&lt;/code&gt; gets fewer wrong answers than &lt;code&gt;amt2&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Set timeouts
&lt;/h2&gt;

&lt;p&gt;An agent will eventually write a query that joins three big tables with no filter. Make sure it can't run for an hour:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;ai_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&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;ROLE&lt;/span&gt; &lt;span class="n"&gt;ai_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;idle_in_transaction_session_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'30s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five seconds is plenty for most business questions. If a question needs more, that's a sign it belongs in a proper report or a pre-computed view, not a live query.&lt;/p&gt;

&lt;p&gt;Role settings apply to &lt;strong&gt;new&lt;/strong&gt; sessions, so reconnect after changing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Limit rows in the tool layer
&lt;/h2&gt;

&lt;p&gt;Nobody needs 2 million rows in a chat answer. Wrap the agent's query before running it:&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;psycopg2&lt;/span&gt;

&lt;span class="n"&gt;MAX_ROWS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only one statement is allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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;SELECT * FROM (&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="s"&gt;) AS agent_q LIMIT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MAX_ROWS&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;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# credentials for the ai_agent role
&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrapping the query in a subquery also means anything that isn't a &lt;code&gt;SELECT&lt;/code&gt; fails with a syntax error before it runs. A &lt;code&gt;DELETE&lt;/code&gt; inside &lt;code&gt;SELECT * FROM (...)&lt;/code&gt; simply isn't valid SQL.&lt;/p&gt;

&lt;p&gt;This is a guardrail, not a security boundary. The read-only role from step 1 is what actually protects your data. The wrapper keeps answers small and stops the agent sending several statements at once. (The semicolon check will also reject a query with a &lt;code&gt;;&lt;/code&gt; inside a string, which is a trade-off we're happy with.)&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Point it at a replica
&lt;/h2&gt;

&lt;p&gt;If you have a read replica, connect the agent there instead of your primary database. Even a well-behaved query can be heavy, and you don't want a curious question about last year's revenue slowing down checkout.&lt;/p&gt;

&lt;p&gt;A replica is also naturally read-only, which gives you one more layer on top of step 1.&lt;/p&gt;

&lt;p&gt;No replica? Lean harder on steps 3 and 4, and consider a lower &lt;code&gt;CONNECTION LIMIT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Log every question, query and answer
&lt;/h2&gt;

&lt;p&gt;When the agent gives a wrong number, you need to know why. Log three things together, with a timestamp and the user who asked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The question the person asked&lt;/li&gt;
&lt;li&gt;The SQL the agent generated&lt;/li&gt;
&lt;li&gt;The answer it returned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the database side, a superuser can log every statement from the agent's role:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;ai_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;log_statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And show the SQL to the person, next to the answer. Anyone who knows the data can then sanity-check it. "Revenue: $1.2M" is easy to trust blindly; "Revenue: $1.2M, from &lt;code&gt;SUM(total_amount) WHERE status = 'paid'&lt;/code&gt;" invites someone to say "that should include refunds".&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it before you trust it
&lt;/h2&gt;

&lt;p&gt;Before you connect an agent, log in as &lt;code&gt;ai_agent&lt;/code&gt; yourself and check the guardrails actually hold:&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;-- Should work&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&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;total_amount&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;reporting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_summary&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Should all fail&lt;/span&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- ERROR: permission denied for table customers&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;default_transaction_read_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;off&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;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- ERROR: permission denied for table orders&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;-- ERROR: canceling statement due to statement timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly what we saw when we ran this setup on PostgreSQL 16: the agent can read the view, and nothing else.&lt;/p&gt;

&lt;p&gt;Then test the answers. Ask the agent 10 questions you already know the answers to, such as last month's revenue, the number of active customers or your top product. Compare. Wrong answers usually point to a confusing column name or a missing view, which you fix in step 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Agent Studio fits
&lt;/h2&gt;

&lt;p&gt;AI Agent Studio lets you connect a database and ask questions in plain English, alongside document search (with sources) and agents that act in 100+ apps. When you connect it, or any similar tool, give it the &lt;code&gt;ai_agent&lt;/code&gt; role and the &lt;code&gt;reporting&lt;/code&gt; schema from this post, not your app's credentials. The setup is the same whichever tool you use.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;How are you handling this?&lt;/strong&gt; If you've connected an AI tool to a real database, what guardrails did you add, and what went wrong first? Tell us in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>postgres</category>
    </item>
    <item>
      <title>The AI agent made my Xcode build green. Here are 6 ways it might have cheated</title>
      <dc:creator>Vishnu JP</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:57:59 +0000</pubDate>
      <link>https://dev.to/iboson/the-ai-agent-made-my-xcode-build-green-here-are-6-ways-it-might-have-cheated-2bhb</link>
      <guid>https://dev.to/iboson/the-ai-agent-made-my-xcode-build-green-here-are-6-ways-it-might-have-cheated-2bhb</guid>
      <description>&lt;p&gt;"Six common 'fixes' AI coding agents make to get a Swift build or test passing, what the real fix looks like, and a pre-commit hook that catches them."&lt;/p&gt;

&lt;p&gt;AI coding agents are very good at making a red build go green. They're less reliable at making it go green &lt;strong&gt;for the right reason&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://phoenix.vu" rel="noopener noreferrer"&gt;Phoenix.vu&lt;/a&gt;, an AI coding agent for Xcode, so we read a lot of agent-written diffs. The same handful of shortcuts comes up again and again, from every model and every tool, including the agents now built into Xcode. None of them are malicious. The agent was asked to fix an error, and these are the fastest ways to make the error disappear.&lt;/p&gt;

&lt;p&gt;Here are six to watch for, with the real fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;@unchecked Sendable&lt;/code&gt; to silence a concurrency error
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The error&lt;/strong&gt; (Swift 6 strict concurrency):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Capture&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;non&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sendable&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="kt"&gt;ImageCache&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="kd"&gt;@Sendable&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="n"&gt;closure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The shortcut:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;ImageCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@unchecked&lt;/span&gt; &lt;span class="kt"&gt;Sendable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIImage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;UIImage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&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;It compiles. It also tells the compiler "trust me, this is thread-safe" when it isn't. You still have a data race on &lt;code&gt;storage&lt;/code&gt;. The compiler has just stopped warning you about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt; make the shared state safe. An actor is the simplest option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;ImageCache&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIImage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;UIImage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Callers now await:&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@unchecked Sendable&lt;/code&gt; has legitimate uses, for example a type protected by a lock. But if the diff adds it without adding a lock, ask why.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;nonisolated(unsafe)&lt;/code&gt; on global state
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The error:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The shortcut:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Settings&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;nonisolated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unsafe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt; decide who owns this state. If it never needs to change, make it a &lt;code&gt;let&lt;/code&gt; of a &lt;code&gt;Sendable&lt;/code&gt; type. If the UI owns it, isolate it to the main actor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@MainActor&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Settings&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Force unwraps to get past optionals
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The error:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Value&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="kd"&gt;optional&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;unwrapped&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The shortcut:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build passes, and the app crashes the first time &lt;code&gt;endpoint&lt;/code&gt; is malformed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;APIError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&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;h2&gt;
  
  
  4. &lt;code&gt;try?&lt;/code&gt; that throws away the error
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The error:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Call can throw but is not marked with 'try'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The shortcut:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a decoding failure silently becomes &lt;code&gt;nil&lt;/code&gt;, and your UI shows an empty profile with no clue why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt; let it throw, or handle it on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&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="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to decode User: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;couldNotLoadProfile&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;code&gt;try?&lt;/code&gt; is fine when &lt;code&gt;nil&lt;/code&gt; really is an acceptable result. It's a problem when it was added only to make the compiler stop complaining.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Disabling the failing test
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The failure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expectation failed: (order.total → 108.0) == 110.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The shortcut:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Flaky"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;totalIncludesTax&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="nv"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="cp"&gt;#expect(order.total == 110)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the test is deleted, or the expected value is changed to match the wrong output. The suite is green, and the bug is now invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt; fix the logic the test is checking. Then, if the test really is flaky, fix the flakiness, for example by removing the dependency on time, the network or ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Making a property optional to "fix" decoding
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The failure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;keyNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CodingKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;stringValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;intValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&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;strong&gt;The shortcut:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decodable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;   &lt;span class="c1"&gt;// was: let price: Decimal&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It decodes, and now every screen that uses &lt;code&gt;price&lt;/code&gt; has to deal with &lt;code&gt;nil&lt;/code&gt;, even though a product without a price is probably a bug in the API response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt; find out why the key is missing. If the API really does omit it sometimes, decode it explicitly with a documented default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decoder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;keyedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CodingKeys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decodeIfPresent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;// API omits price for free items&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A 30-second review checklist
&lt;/h2&gt;

&lt;p&gt;Before you accept an agent's fix, search the diff for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@unchecked Sendable&lt;/code&gt; or &lt;code&gt;nonisolated(unsafe)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;!&lt;/code&gt; force unwraps, &lt;code&gt;try!&lt;/code&gt; or &lt;code&gt;as!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;try?&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.disabled(&lt;/code&gt;, &lt;code&gt;XCTSkip&lt;/code&gt;, or removed test functions&lt;/li&gt;
&lt;li&gt;Properties that became optional&lt;/li&gt;
&lt;li&gt;Large deletions in a "small" fix&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bonus: a pre-commit hook that flags them
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;.git/hooks/pre-commit&lt;/code&gt; and make it executable (&lt;code&gt;chmod +x .git/hooks/pre-commit&lt;/code&gt;):&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="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Flags common "make it compile" shortcuts in staged Swift changes.&lt;/span&gt;
&lt;span class="nv"&gt;PATTERNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'@unchecked Sendable|nonisolated\(unsafe\)|try!|as!|\.disabled\(|XCTSkip'&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;-U0&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*.swift'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^+'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"These added lines often hide a real problem. Review them,"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"or commit with --no-verify if they're intentional."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It won't catch everything, but it catches the most common cases, whoever wrote the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Phoenix fits
&lt;/h2&gt;

&lt;p&gt;We built Phoenix around this review step. It runs the build-fix loop alongside Xcode, but every change is shown as a diff that you approve or reject before it's applied, and it snapshots each file before writing so you can undo without Git. It's pay-per-task with no subscription, and your code stays on your Mac.&lt;/p&gt;

&lt;p&gt;If you already pay for Claude or ChatGPT, Xcode's built-in agents are a good place to start, and everything above applies to them too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the worst "fix" you've seen an AI agent make to get a build passing?&lt;/strong&gt; Share it in the comments. We'll add the best ones to the checklist.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Spotted something that behaves differently in your Xcode version? Let us know in the comments and we'll update the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>ai</category>
    </item>
    <item>
      <title>We build 5 AI products. All of them follow one rule: the agent shows you before it acts</title>
      <dc:creator>Vishnu JP</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/iboson/we-build-5-ai-products-all-of-them-follow-one-rule-the-agent-shows-you-before-it-acts-35l4</link>
      <guid>https://dev.to/iboson/we-build-5-ai-products-all-of-them-follow-one-rule-the-agent-shows-you-before-it-acts-35l4</guid>
      <description>&lt;p&gt;How a small team designs AI agents that do real work (code, marketing, phone calls, video, AR) while people keep the final say.&lt;/p&gt;

&lt;p&gt;We're iBoson Innovations, a small product company. We started in augmented reality, and today we build five AI products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix.vu&lt;/strong&gt;: an AI coding agent that works alongside Xcode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lyks.ai&lt;/strong&gt;: AI product videos for Shopify stores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Agent Studio&lt;/strong&gt; (&lt;a href="https://aiagents.dev" rel="noopener noreferrer"&gt;aiagents.dev&lt;/a&gt;): no-code agents that answer calls and act in business apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blooming.sh&lt;/strong&gt;: 40+ AI agents for marketing, sales and support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NoxVision.ai&lt;/strong&gt;: real-time 3D object recognition for AR apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is our first post on DEV, so instead of a feature tour, I want to talk about the one design rule all five products share. We think it's the most important decision in any agent product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents fail differently from chatbots
&lt;/h2&gt;

&lt;p&gt;There's a lot of scepticism about AI agents here right now, and much of it is fair.&lt;/p&gt;

&lt;p&gt;When a chatbot is wrong, you get a wrong answer. You read it, frown, and ignore it. When an agent is wrong, something changes: code gets rewritten, an email goes to a client, a ticket gets closed, a call ends with the wrong booking. The cost of a mistake moves from "I wasted 30 seconds" to "now I have to find out what it did and undo it".&lt;/p&gt;

&lt;p&gt;That's why "fully autonomous" was never our goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our rule: show, then act
&lt;/h2&gt;

&lt;p&gt;Every iBoson product follows the same loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent does the work and produces a &lt;strong&gt;concrete proposal&lt;/strong&gt;: a diff, a draft, a script, an action.&lt;/li&gt;
&lt;li&gt;A person &lt;strong&gt;reviews and approves&lt;/strong&gt; it.&lt;/li&gt;
&lt;li&gt;Only then is it &lt;strong&gt;applied&lt;/strong&gt;, and it can be &lt;strong&gt;undone&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's how that looks across the five products:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;What the agent does&lt;/th&gt;
&lt;th&gt;What you review before it counts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phoenix.vu&lt;/td&gt;
&lt;td&gt;Writes Swift/SwiftUI, runs the build, fixes errors&lt;/td&gt;
&lt;td&gt;A diff for every change, plus file snapshots before each write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blooming.sh&lt;/td&gt;
&lt;td&gt;Drafts posts, emails, sequences and support replies&lt;/td&gt;
&lt;td&gt;An approval step per client brand before anything is published or sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Agent Studio&lt;/td&gt;
&lt;td&gt;Answers calls, queries data, acts in 100+ apps&lt;/td&gt;
&lt;td&gt;Approval steps on the actions you choose, plus handover rules for calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lyks.ai&lt;/td&gt;
&lt;td&gt;Writes a script from your product listing and generates a video&lt;/td&gt;
&lt;td&gt;The script, which you can edit before anything is rendered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NoxVision.ai&lt;/td&gt;
&lt;td&gt;Detects real objects through the phone camera&lt;/td&gt;
&lt;td&gt;Your app decides what happens on detection; models stay private to your account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;NoxVision is the odd one out. It's a perception SDK, not an agent that takes actions. But the same idea applies: it tells your app what it sees, and your code decides what to do with that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things we learned building it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Make the proposal the unit of work
&lt;/h3&gt;

&lt;p&gt;"The agent did some stuff" is impossible to review. A diff, a draft, or a numbered list of actions is easy to review. We design every agent around producing something with a clear before and after.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Make undo cheap
&lt;/h3&gt;

&lt;p&gt;If undo is expensive, people stop trusting approvals, because one bad click costs too much. Phoenix snapshots each file before it writes to it, so you can roll back without relying on Git. Blooming keeps drafts separate from what's published.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Show the cost of every action
&lt;/h3&gt;

&lt;p&gt;Agents can burn money quietly. Phoenix and Lyks both use pay-as-you-go credits instead of subscriptions, and Phoenix shows the exact cost after each request. Seeing "this fix cost 4 credits" changes how people use an agent. It also keeps us honest about whether each step was worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Don't ask for approval on everything
&lt;/h3&gt;

&lt;p&gt;Approval fatigue is real. If you ask someone to approve every tiny step, they start clicking "yes" without reading, which is worse than no approval at all. So the rule is about &lt;em&gt;risk&lt;/em&gt;, not every step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only work runs freely&lt;/strong&gt;: searching documents, answering questions from a read-only database connection, reading build logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes need approval&lt;/strong&gt;: writing code, publishing content, sending messages, updating records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users choose the line&lt;/strong&gt;: in AI Agent Studio you decide which actions need a human.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Something we got wrong
&lt;/h2&gt;

&lt;p&gt;We built Phoenix into a gap: an Xcode-native coding agent, just as a well-liked tool in that space shut down. Then Xcode 26.3 shipped its own built-in agents from Anthropic and OpenAI. A big part of our pitch became something Xcode does by default.&lt;/p&gt;

&lt;p&gt;We learned to position against what's coming, not just against what's missing today. We now tell developers who already pay for Claude or ChatGPT to try Apple's agents first. Phoenix is for people who want pay-per-task pricing and a diff-first workflow. Being fair about that has made the rest of what we say more believable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'll write about here
&lt;/h2&gt;

&lt;p&gt;One practical post a week, rotating through our products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviewing AI-made fixes in Xcode, so a green build isn't hiding a bad change&lt;/li&gt;
&lt;li&gt;Giving an AI agent database access safely&lt;/li&gt;
&lt;li&gt;AR object recognition without training an ML model&lt;/li&gt;
&lt;li&gt;AI video for e-commerce: what works and what doesn't&lt;/li&gt;
&lt;li&gt;Running marketing with multiple agents without losing a brand's voice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll share code, trade-offs and mistakes, and say when another tool is the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Over to you
&lt;/h2&gt;

&lt;p&gt;Where do you draw the line? Which actions would you let an AI agent take &lt;strong&gt;without&lt;/strong&gt; asking you first, and which should always need a human? Tell us in the comments. We're using the answers to tune our defaults.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>discuss</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
