كيفية تقليل تكاليف رموز الوكيل من سطر الأوامر (دليل 2026)

يشعر وكيل البرمجة عبر واجهة سطر الأوامر (CLI) بالحرية إلى أن تصل الفاتورة. تشغّل Claude Code أو Codex داخل مستودع، تطلب إعادة هيكلة وحدة، وبعد عشر دقائق يكون الوكيل قد قرأ عشرات الملفات، شغّل الاختبارات أكثر من مرة، وأرسل سياقًا لم تكن بحاجة إليه أصلًا. على مستوى فريق من عدة مهندسين، يتحول هذا إلى تكلفة توكنات واضحة. الحل ليس بالضرورة تغيير النموذج أو قبول نتائج أسوأ، بل تقليل ما يصل إلى النموذج من البداية.

جرّب Apidog اليوم

باختصار

لتقليل تكلفة توكنات وكلاء البرمجة:

  • قلّص السياق قبل إرساله إلى النموذج.
  • حدّد الملفات والدلائل التي يعمل عليها الوكيل.
  • اجعل ملفات الذاكرة مثل CLAUDE.md قصيرة ومستقرة.
  • استخدم /compact أو /clear بين المهام.
  • فعّل prompt caching للبادئات المستقرة.
  • وجّه المهام البسيطة إلى نموذج أرخص.
  • قلّل إخراج الأدوات مثل الاختبارات وnpm install.
  • قِس التكلفة لكل تشغيل بدل الاعتماد على الفاتورة الشهرية فقط.

مقدمة

تظهر المشكلة غالبًا بطريقتين:

  1. تصل إلى حد أسبوعي أو حد جلسة في منتصف العمل.
  2. تصل فاتورة API ويسأل الفريق لماذا أصبح “مساعد الذكاء الاصطناعي” بندًا مكلفًا.

السبب واحد: وكلاء CLI يستهلكون التوكنات افتراضيًا. يقرؤون ملفات كاملة بدل عشرة أسطر، يعيدون إرسال سجل المحادثة في كل دور، يضعون مخرجات الأوامر الخام داخل السياق، ويرسلون نفس موجه النظام وخريطة المشروع مرارًا.

إعادة هيكلة تحتاج فعليًا إلى 2,000 توكن من الكود لا يجب أن تستخدم 180,000 توكن من السياق. الفرق بين الرقمين هو مساحة التوفير.

في هذا الدليل سنحوّل تقليل التوكنات إلى خطوات عملية:

  • تنظيف السياق وملفات الذاكرة.
  • استخدام prompt caching.
  • توجيه النماذج حسب نوع المهمة.
  • تقليل إخراج الأدوات والاسترجاع.
  • قياس التكلفة لكل تشغيل.

الأمثلة تفترض Claude Code وCodex، لكن الفكرة تنطبق على أي وكيل CLI يتصل بواجهة API تُحاسب بالتوكنات.

💡 إذا كان وكلاؤك يتعاملون مع APIs، فإن تصميمها ومحاكاتها واختبارها في Apidog قبل توجيه الوكيل إليها يقلل التجربة والخطأ المكلفة. يعمل الوكيل ضد عقد API واضح ومستقر بدل نقطة نهاية حية تفاجئه بأخطاء متغيرة.

إلى أين تذهب التوكنات عند تشغيل وكيل CLI؟

كل دور في جلسة الوكيل يرسل حمولة إدخال إلى النموذج ويحصل على حمولة إخراج. أنت تدفع مقابل الاثنين.

عادةً تكون توكنات الإخراج أغلى من الإدخال، لكن الإدخال هو الذي يتضخم بسرعة.

حمولة الإدخال تتكوّن غالبًا من:

  • موجه النظام وتعريفات الأدوات

    تعليمات الوكيل ومخططات JSON الخاصة بالأدوات. قد تكون من 5,000 إلى 15,000 توكن وتُعاد في كل دور.

  • ملفات الذاكرة والمشروع

    مثل CLAUDE.md أو تعليمات المشروع الدائمة. تُحمّل حتى عندما لا تكون مرتبطة بالمهمة.

  • سجل المحادثة

    كل رسالة مستخدم، رد نموذج، استدعاء أداة، ونتيجة أداة يعاد إرسالها. هذا عادةً أكبر مصدر للهدر في الجلسات الطويلة.

  • محتوى الملفات المقروءة

    قراءة ملف من 1,200 سطر قد تعني 12,000 إلى 18,000 توكن.

  • إخراج الأدوات

    سجلات الاختبارات، npm install، git diff، stack traces، وملفات القفل.

النقطة الأهم: سجل المحادثة يُعاد تشغيله في كل دور. جلسة من 30 دورًا لا تكلف 30 مرة فقط، بل تكلف مجموع بادئة تكبر مع الوقت. لذلك الجلسات الطويلة غير المضغوطة هي من أغلى أنماط الاستخدام.

لمزيد من التفاصيل حول حدود الجلسات وحساب التوكنات، راجع: كيفية إعادة تعيين نافذة توكنات كلود كود.

1. نظافة السياق وملفات الذاكرة

أرخص توكن هو التوكن الذي لا ترسله أصلًا.

حدّد نطاق العمل قبل تشغيل الوكيل

لا تبدأ من جذر المستودع بسؤال عام مثل:

claude "أعد هيكلة منطق الفوترة"

هذا يدفع الوكيل للبحث في المستودع كاملًا.

اكتب مهمة محددة:

claude "أعد هيكلة منطق إعادة المحاولة ليستخدم التراجع الأسي.
اعمل فقط على:
- src/payments/retry.ts
- src/payments/retry.test.ts"

إذا كنت لا تعرف الملف بدقة، حدّد الدليل بدل الجذر:

claude "ابحث داخل src/payments فقط عن منطق إعادة المحاولة، ثم اقترح تعديلًا محدودًا"

اجعل CLAUDE.md قصيرًا

ملف الذاكرة مثل CLAUDE.md يُحمّل في كل دور. إذا تحول إلى wiki داخلية، ستدفع ثمنه مرارًا.

افحص حجمه تقريبيًا:

wc -c CLAUDE.md | awk '{print "≈", int($1/4), "توكن لكل دور"}'

اجعل الملف يحتوي فقط على:

  • أوامر البناء والاختبار الأساسية.
  • قواعد الكود الصارمة.
  • بنية المشروع المختصرة.
  • روابط أو مسارات للوثائق الطويلة بدل نسخها داخله.

مثال جيد:

# تعليمات الوكيل

## أوامر
- npm test --silent
- npm run lint

## قواعد
- لا تعدّل ملفات dist/
- لا تغيّر API عام بدون تحديث الاختبارات
- استخدم TypeScript strict types

## وثائق إضافية
- API contracts: docs/api/
- Payment flow: docs/payments.md

اضغط أو امسح الجلسات الطويلة

عندما تنتهي مهمة وتبدأ أخرى، لا تكمل في نفس السياق.

في Claude Code:

/compact

يستبدل السجل الطويل بملخص قصير.

أو:

/clear

يبدأ جلسة جديدة بدون حمل السياق القديم.

قاعدة عملية:

  • مهمة واحدة = جلسة واحدة.
  • بعد الانتهاء: /compact إذا ستكمل على نفس الهدف.
  • استخدم /clear إذا ستنتقل إلى هدف مختلف.

راجع أيضًا: سير عمل كلود كود.

استخدم ملف تجاهل

امنع الوكيل من قراءة ما لا يحتاجه:

node_modules/
dist/
build/
coverage/
.next/
.cache/
*.lock

إذا كان وكيلك يدعم ملف تجاهل مخصص، أضف أيضًا:

docs/generated/
tmp/
vendor/

الهدف: لا تجعل الوكيل يرى ملفات ضخمة لن يعدّلها.

2. Prompt caching: لا تدفع السعر الكامل لنفس البادئة

Prompt caching يسمح للمزود بتخزين بادئة ثابتة من الطلب، مثل:

  • موجه النظام.
  • تعريفات الأدوات.
  • اتفاقيات المشروع.
  • التعليمات المستقرة.

ثم تُقرأ هذه البادئة لاحقًا بخصم كبير بدل إعادة معالجتها بالكامل.

وفقًا لوثائق Anthropic، قراءة cache قد تكلف تقريبًا 10% من تكلفة الإدخال الأساسي، بينما كتابة cache تكلف أكثر قليلًا من الإدخال العادي. لذلك يصبح مفيدًا بسرعة عندما تعيد استخدام نفس البادئة.

القاعدة

ضع المحتوى المستقر أولًا، والمتغير آخرًا.

الترتيب العام:

tools → system → messages

لا تضع طابعًا زمنيًا أو بيانات متغيرة قبل نقطة التخزين المؤقت.

مثال Python

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT + REPO_CONVENTIONS,
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=[
        {
            "role": "user",
            "content": user_task,
        }
    ],
)

u = response.usage

print("cache write:", u.cache_creation_input_tokens)
print("cache read :", u.cache_read_input_tokens)
print("fresh input:", u.input_tokens)

تحقق من أن cache يعمل

إذا كان cache_read_input_tokens يساوي صفرًا دائمًا، فغالبًا لديك أحد هذه الأخطاء:

  • تغيّر البادئة في كل طلب.
  • أضفت timestamp داخل موجه النظام.
  • وضعت محتوى الملف المتغير قبل نقطة cache.
  • البادئة أصغر من الحد الأدنى القابل للتخزين.

اجعل موجه النظام واتفاقيات المشروع ثابتة byte-for-byte قدر الإمكان.

للاستفادة من التوجيه مع Codex، راجع: تشغيل GPT-5.5 مجانًا عبر Codex.

3. توجيه النموذج: استخدم نموذجًا رخيصًا للمهام الرخيصة

ليست كل مهمة تحتاج النموذج الأقوى.

مهام غالبًا يمكن تنفيذها بنموذج أرخص:

  • كتابة commit message.
  • تلخيص git diff.
  • إنشاء changelog.
  • شرح خطأ lint.
  • إعادة تسمية بسيطة.
  • كتابة اختبار نمطي.

مثال:

claude --model haiku "اكتب رسالة commit بنمط conventional commit للتغييرات المرحلية"

claude --model sonnet "أعد تصميم طبقة التخزين المؤقت لخدمة المدفوعات"

اجعل الافتراضي رخيصًا، وصعّد عند الحاجة:

alias claude-cheap='claude --model haiku'
alias claude-strong='claude --model sonnet'

ثم استخدم:

claude-cheap "لخص هذا diff في 5 نقاط"
claude-strong "حلل سبب race condition في طبقة الدفع"

استخدم وكلاء فرعيين بسياق صغير

إذا كان نظامك يدعم sub-agents، اجعل الوكيل الفرعي يعمل على مهمة ضيقة بنموذج رخيص، ثم يعيد ملخصًا قصيرًا للوكيل الرئيسي.

بدلًا من أن يرى النموذج المكلف 30,000 توكن من البحث، يرى 700 توكن من الخلاصة.

مثال نمط عمل:

الوكيل الرئيسي:
- يحدد السؤال.
- يطلب من وكيل فرعي رخيص البحث في src/payments فقط.
- يستقبل ملخصًا قصيرًا.
- يستخدم النموذج الأقوى لاتخاذ القرار النهائي.

راجع: أمر الهدف عبر وكلاء كوديكس وكلود كود المستقلين.

إذا كنت تعمل ضمن حدود أسبوعية، فهذا لا يوفر المال فقط، بل يطيل عمر الحصة. راجع أيضًا: زيادة الحد الأسبوعي لكلود كود الأخيرة.

4. قلّل إخراج الأدوات والاسترجاع

كل سطر تطبعه أداة يمكن أن يدخل السياق، ثم يُعاد في الأدوار اللاحقة.

اجعل الأوامر صامتة

بدل:

npm test

استخدم:

npm test --silent -- --reporter=dot

بدل:

npm install

استخدم:

npm install --silent --no-audit --no-fund

فلتر المخرجات قبل أن يراها الوكيل

pytest -q 2>&1 | tail -n 30
git diff --stat
npm test 2>&1 | grep -E "(FAIL|✗|Error)" | head -n 20

بدل إرجاع سجل اختبار كامل، أعد آخر 30 سطرًا فقط أو الأسطر التي تحتوي على الخطأ.

استخدم قراءة مستهدفة بدل قراءة ملف كامل

اطلب من الوكيل صراحة:

ابحث عن الدالة التي تتعامل مع retry.
اقرأ فقط 80 سطرًا حولها.
لا تقرأ الملف كاملًا إلا إذا لم يكفِ ذلك.

أو استخدم أنت أدوات CLI:

grep -n "function retry" src/payments/retry.ts
sed -n '40,130p' src/payments/retry.ts

فرق التكلفة قد يكون كبيرًا: قراءة 100 سطر بدل 1,500 سطر.

قيّد RAG والاسترجاع

إذا كان الوكيل يسترجع من docs أو code search، لا تسمح له بجلب عشرات القطع الكبيرة.

إعداد عملي:

max_chunks: 8
chunk_size: 300 tokens

أفضل من:

max_chunks: 50
chunk_size: 800 tokens

أنت تدفع مقابل كل قطعة مسترجعة حتى لو لم يستخدمها النموذج.

5. قِس تكلفة التشغيل

لا تعتمد على الانطباع. سجّل التكلفة لكل نوع مهمة.

إذا كنت تستدعي API مباشرة، استخدم كائن usage:

u = response.usage

INPUT_RATE  = 3.00 / 1_000_000
OUTPUT_RATE = 15.00 / 1_000_000
CACHE_READ  = 0.30 / 1_000_000
CACHE_WRITE = 3.75 / 1_000_000

cost = (
    u.input_tokens * INPUT_RATE +
    u.output_tokens * OUTPUT_RATE +
    u.cache_read_input_tokens * CACHE_READ +
    u.cache_creation_input_tokens * CACHE_WRITE
)

print(
    f"تكلفة التشغيل ≈ ${cost:.4f} "
    f"(in={u.input_tokens} out={u.output_tokens} "
    f"cache_read={u.cache_read_input_tokens})"
)

الأسعار هنا توضيحية. استخدم أسعار النموذج الحالية من المزود.

إذا كنت تستخدم CLI فقط:

claude /cost

أو افصل مفاتيح API حسب المشروع:

API key لكل وكيل أو مشروع
→ لوحة تحكم المزود تعرض الإنفاق بشكل قابل للتتبع

أو غلّف تشغيل الوكيل بسكربت يسجل المهمة:

#!/usr/bin/env bash

TASK="$1"
START=$(date -Iseconds)

claude "$TASK"

END=$(date -Iseconds)

echo "$START,$END,$TASK" >> agent-runs.csv

ثم أضف التكلفة المبلغ عنها يدويًا أو من API لاحقًا.

قِس قبل وبعد

اختر مهامًا تمثيلية:

  • إعادة هيكلة يومية.
  • مراجعة PR.
  • توليد اختبار.
  • تلخيص diff.

وسجّل:

task,model,input_tokens,output_tokens,cache_read,cost

بعد تطبيق أي تحسين، يجب أن ترى الرقم يتحرك. إذا لم يتحرك، فالتحسين لا يعالج مصدر الهدر الحقيقي.

مقارنة التكتيكات

التكتيك الوفورات النموذجية الجهد
تحديد نطاق الملفات والدلائل 30–60% من الإدخال لكل تشغيل منخفض
تقصير ملف الذاكرة 5–15% لكل دور منخفض
استخدام /compact أو /clear 40–80% في الجلسات الطويلة منخفض
prompt caching للبادئة المستقرة ~90% على الجزء المخزن متوسط
توجيه المهام البسيطة إلى نموذج أرخص 50–80% على المهام الموجهة متوسط
إخراج أدوات هادئ ومفلتر 20–50% في التشغيلات المعتمدة على الأدوات منخفض
قراءة مستهدفة بدل ملفات كاملة 70–95% في الملفات الكبيرة منخفض
تقليل حجم الاسترجاع/RAG 30–60% في وكلاء البحث متوسط
قياس التكلفة لكل تشغيل لا يوفر مباشرة، لكنه يكشف الهدر منخفض

الأرقام تقريبية وتعتمد على شكل استخدامك، لكنها تساعدك على ترتيب الأولويات.

قائمة تنفيذ سريعة

ابدأ بهذه الخطوات اليوم:

# 1. افحص حجم ملف الذاكرة
wc -c CLAUDE.md | awk '{print "≈", int($1/4), "توكن لكل دور"}'

# 2. استخدم اختبارات هادئة
npm test --silent -- --reporter=dot

# 3. قلل diff
git diff --stat

# 4. امسح الجلسة عند تغيير المهمة
/clear

# 5. اضغط الجلسة الطويلة
/compact

ثم طبّق على مستوى الفريق:

  • راجع CLAUDE.md مرة أسبوعيًا.
  • أضف ملفات التجاهل.
  • اجعل النموذج الأرخص هو الافتراضي للمهام الميكانيكية.
  • فعّل prompt caching إن كنت تتحكم في استدعاءات API.
  • سجّل تكلفة المهام المتكررة.

الخاتمة

تكلفة توكنات وكلاء البرمجة ليست مشكلة حتمية. معظم الهدر يأتي من سياق زائد، جلسات طويلة، مخرجات أدوات صاخبة، ونموذج أقوى من المطلوب.

ابدأ بالأشياء منخفضة الجهد: تحديد النطاق، إخراج هادئ، ملف ذاكرة صغير، و/compact بين المهام. بعد ذلك أضف prompt caching وتوجيه النماذج. النتيجة عادةً تكون فاتورة أقل بدون التضحية بجودة العمل.

OpenClaw and Claude Code – Multi Agents talking via Handoff File

A few months ago I finished setting up my Raspberry Pi with metrics, alerting, domain access, and secrets, and ended up with a pretty neat device that did nothing. Around the same time OpenClaw started showing up in my feed — a bot with access to an LLM that could sit on a small device and do work for you. The pitch was good enough that I gave it a weekend.

A weekend turned into months of configurations hell and confusion. The thing I underestimated was how big the gap is between “an LLM bot responds on Discord” and “an LLM bot does real engineering work for me.” A bot that can chat is easy. A bot that can take a Discord message, decide it needs to write code, write that code, run tests, open a PR, and tell me about it that is a different category of system.

An LLM bot does real engineering work for me..

That shift surfaces three problems that the cosy “ask the LLM a question” framing hides. Given my experience in building agents, I wanted to avoid the context rots and instruction overload problems from the start. Additionally, I had been pretty comfortable using Claude Code for coding tasks. So OpenClaw had to come-in for something else.

If you have are agents builder, you will recognise that building a multi agent system is the answer to the problems above. The killer feature of OpenClaw for me is the chat integrations which comes at a low cost so it felt really compelling.

The separation becomes clear:

  • OpenClaw handles user interaction
  • Claude Code handles writing code.

Unsurpisingly this is a setup that has been proven in the past by this tweet https://x.com/elvissun/status/2025920521871716562, which has been an inspiration and drove me to reproduce the setup.

Building Coding Assistant

Here is the shape of it, before getting into the why:

Two agents, one file hand-off between them. OpenClaw is the long-lived assistant on Discord. Claude Code is spawned per task, does the work, opens a PR, exits. The filesystem is just a folder — OpenClaw drops a task in, Claude Code picks it up, drops the result back. OpenClaw also listens for PR events from GitHub as a heartbeat, so it can confirm the work actually landed and not just that Claude Code said it did.

The rest of the post is the why and the how.

Three protocols, one survivor

The job is straightforward to describe. OpenClaw understands the user request from Discord, decides this needs real code written, and hands the work off to Claude Code, which produces a PR. OpenClaw watches for completion and reports back.

The whole problem is the box in the middle.

The hard part is the hand-off. I tried three using a shared tmux session and a filesystem via handoff file, and with an ACP (Agent Communication Protocol):

  1. ACP (Agent Communication Protocol). The “right” way. Sessions, capabilities, structured messages, the whole thing. I had this working in Part 4 with Codex. It worked. It also fell over under load, hit timeouts, and depended on a pairing dance that broke every time the Pi rebooted.
  2. A shared tmux session. Skip the protocol entirely, drive a real terminal that the worker lives inside. Capture the pane to read output. It works. It is also extremely fragile — anything that touches the terminal state (a stray newline, an ANSI escape, a pager that opens) corrupts the channel.
  3. A filesystem mailbox. OpenClaw writes a JSON file. inotifywait sees the file. A systemd service launches Claude Code with -p against that file. Claude Code writes the result to another JSON file. Done.

I have spent weeks trying to fine-tune the ACP and Tmux, until I found inotifywait. The rest of this post is how that is wired up and why the others didn’t work for me.

OpenClaw under systemd

Although all of the documentations of OpenClaw and Claude Code omit that, there is a hidden truth about agents – they are system processes. Which means, they will die eventually, silently – unless you deamonise them.

We will handle it using a systemd file. The relevant guarantees from systemd:

  • Restart=on-failure — the process comes back if it crashes
  • systemctl --user enable makes it survive reboot

Think about systemd like a docker container, but it lives on your bare metal. A typical systemd file looks like that:

[Unit]
Description=OpenClaw Bot
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
User={{ bot_user }}
WorkingDirectory={{ bot_home }}
EnvironmentFile={{ env_file }}
Environment=REPO_DIR={{ repo_dir }}
Environment=FEATURE_DOCS_DIR={{ feature_docs_dir }}
ExecStart={{ bot_home }}/scripts/start.sh
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Start it with


systemctl --user daemon-reload
systemctl --user enable --now openclaw-gateway.service
journalctl --user -u openclaw-gateway -f

And see the logs

systemctl --user status openclaw-gateway.service
● openclaw-gateway.service - OpenClaw Gateway
     Loaded: loaded (/home/vvasylkovskyi/.config/systemd/user/openclaw-gateway.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-05-18 22:14:07 BST; 17h ago

With systemd in place, OpenClaw is a thing that stays up. That is the floor.

Claude Code, watched by inotifywait, supervised by systemd

The worker side is its own systemd service, similar to the one above. Except in this case, ExecStart={{ bot_home }}/scripts/start.sh will:

  1. Start a persistent inotifywait loop watching the filesystem directory
  2. inotifywait triggers a shell script that, on file creation, launches Claude Code in headless mode
  3. A systemd service that owns the watcher and restarts it if it dies

The watcher loop is the entire orchestration logic on the worker side. There is nothing fancy in here:

#!/usr/bin/env bash
set -euo pipefail

# ── Watcher loop ──────────────────────────────────────────────────────────────
echo "[pipeline] Watching $FEATURE_DOCS_DIR for new feature docs..."
cd "$REPO_DIR"

CLAUDE_BIN=$(find /usr/local/bin "$HOME/.local/bin" "$HOME/.npm-global/bin" -name claude 2>/dev/null | head -1)

inotifywait -m "$FEATURE_DOCS_DIR" -e create | while read -r path _event file; do
  echo "[pipeline] Detected: $file — starting pipeline..."
  "$CLAUDE_BIN" --dangerously-skip-permissions -p 
    "/implementation-orchestrator Process this feature doc: @${path}${file}"
  echo "[pipeline] Done: $file"
done

When the new file is created, claude code is spawned and reads the file. The claude --dangerously-skip-permissions -p ensures that the headless mode is activated, and there are no confirmation prompts – human completely out of the loop.

Skill fine-tuned for human-out-of-the-loop

The message is simple: "/implementation-orchestrator Process this feature doc: @${path}${file}". So whatever is in the file is the direction. The policy is the skill where we have to stir agent from asking questions with prompts like:

You are an orchestration agent running in a fully automated headless pipeline.
You must never pause for input, never ask clarifying questions, and never wait for human confirmation mid-session.

That is the entire worker. A watcher, a launcher, and a folder with markdown files. The interesting design choices are not in the code – they are in what I am deliberately not doing.

Protocols that did not work

It is worth being explicit about what I tried and why I abandoned it, because the file handoff looks underwhelming until you have lived with the alternatives.

Why we eventually pivoted from ACP to tmux

ACP has a lot of open issues — sessions timing out, the gateway pairing dance, the arm64 binary problem. After getting it working, I kept hitting reliability problems. There are also open GitHub issues specifically about this:

Under heavy load, the ACP system would overload:

tmux

The pivot from ACP was to a shared tmux session. Alegendly one can spawn tmux sessions and start in them the coding agents:

tmux list-windows -t agents | grep -q claude || 
  tmux new-window -t agents -n claude

Then start the claude session using the below:

tmux send-keys -t agents:claude 
  "claude --agent-file ~/.claude/CLAUDE.md" Enter

Spawn the worker inside tmux, drive it by writing to its stdin via tmux send-keys, capture results with tmux capture-pane -p. This works… until it doesn’t, and when it doesn’t, the failure is in terminal not in logs.

The problem is executing the send-keys command reliably. Sometimes double enter is required, and often it is not enough. More importantly, when something fails, the error trace is not available unless we manually ssh into device and attach to the tmux session – if the session is alive. If the tmux server dies for any reason (memory pressure, a crashed pane), every running task is lost with no recovery path.

The general lesson, which I have now learned twice, is that any IPC channel that goes through a TTY is a TTY. You do not get to pretend it is a structured stream. The terminal will assert itself eventually.

Why using inotify is better

Stepping back from the wiring, the reason the inotifywait won is not that it is fast or elegant. It is because the communication is battle-proven and dead simple. The watcher is reliable, and as long as the file drops in the folder, the watcher will spawn the agent. The rest of the components might fail independently but each failure has an obvious recovery. The inotifywait can also have failure modes, but they all show up in either the Discord channel or journalctl, both of which I check anyway.

When OpenClaw dropped the file on the system, the logs show like these.

Apr 28 15:59:29 raspberry-4b systemd[1]: Started claude-coding-bot.service - Claude Code PR Bot.
Apr 28 15:59:29 raspberry-4b start.sh[2514944]: [pipeline] Watching /home/vvasylkovskyi/my-claude-code/context/feature-docs for new feature docs...
Apr 28 15:59:29 raspberry-4b start.sh[2514950]: Setting up watches.
Apr 28 15:59:29 raspberry-4b start.sh[2514950]: Watches established.
Apr 28 15:59:54 raspberry-4b start.sh[2514951]: [pipeline] Detected: print-readme-testing-headless-claude-code.md — starting pipeline...
Apr 28 15:59:59 raspberry-4b start.sh[2515185]: Warning: no stdin data received in 3s, proceeding without it. If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer.
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: `🎉 Final PR opened: https://github.com/vvasylkovskyi/my-app/pull/2`
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: ---
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: ```
{% endraw %}

Apr 28 16:03:24 raspberry-4b start.sh[2515185]: === Autopilot Session Complete ===
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: Mode: features
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: Repo: /home/vvasylkovskyi/my-claude-code/repo
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: ✅  print-readme-testing-headless-claude-code → PR #1 merged → feature/autopilot-2026-04-28
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: Skipped:   0
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: Completed: 1
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: Failed:    0
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: ==================================
Apr 28 16:03:24 raspberry-4b start.sh[2515185]: Final PR for your review: https://github.com/vvasylkovskyi/my-app/pull/2

{% raw %}

The ACP and tmux versions of had failure modes that were silent or required SSH. Filesystems are used by Claude Code – They can also use tmux to multiplex your open session. However in headless mode, the TTY is unreliable due to its inherent interactive nature.

What I would tell myself before starting

Multi agents architectures have little to do with AI. What matters are the deep skills in linux and building a distributed system. The filesystem communication pattern is a dead-simple, and the only approach that I didn’t have to fix.

Regarding tmux, the assumption that you can drive a terminal as if it were a structured channel is wrong. It works in the demo but it breaks the moment a tool decides to be helpful and open a pager, or the keys-send didn’t send the space for the tmux session to submit question to llm. The tmux approach is fine for known, bounded outputs and dangerous for anything else.

The filesystem watcher is not a clever design. It is a design that takes the things Linux is already good at – files, services, watchers – and arranges them so that the agent layer only has to do the parts that genuinely need an agent. Why is it so convenient? Well, because we are on the same device. Sometimes the simplest solutions are the ones taking the longest to reveal themselves.

Why your accessibility tools can’t see your diagram

Org chart app with a node focused, showing a visible focus ring

TL;DR:

  • Lighthouse scored my org chart 98%. A keyboard user could not reach a single node.
  • I audited from three angles: automated tools, manual pass, LLM review.
  • Fixes: landmarks, headings, focus styles, role="tree" + custom keyboard shortcuts, skip-link as documentation.
  • Tools score documents, not diagrams. They check what they can check.
  • The biggest takeaway: try the app with the keyboard before the first commit.

Accessibility is getting harder to ignore, and that is mostly a good thing. The same markup that helps a screen reader user also lets web crawlers and LLM agents work from structure instead of screenshots. The catch is that the apps where the standard tools and articles say the least – diagrams, dashboards, data-heavy interfaces – are the ones where teams will increasingly need answers. The complexity is real, but it is not an excuse.

I built an org chart with ng-diagram a while back, and I want accessibility to be part of how I build rather than something I bolt on later. To audit it I picked the tools I had not used before on a real app: Lighthouse, WAVE, an accessibility linter, and an LLM-based review. What follows is a walkthrough of that audit rather than a guide.

Org chart app with a node focused, showing a visible focus ringThe org chart after the audit – a node has keyboard focus, with a visible focus ring.

The audit

The plan was to look at the app from three angles: automated tools, a manual pass, and an LLM review of the code itself.

What the automated tools said

Lighthouse went first. 98 out of 100, with one fix (a missing main landmark) and a manual checklist worth running through.

WAVE was more useful. Zero errors, but the warnings were the interesting part: node descriptions rendered below comfortable reading size and no heading structure on the page at all. The order tab was the only thing nagging me – something about the labels on the diagram’s tab stops did not quite line up with what I expected, and that was enough to push me into trying the app manually.

WAVE order tab overlay numbering focusable elements 1, 2, 3... across the toolbar before the canvasWAVE’s order tab numbering the focusable elements. The canvas is buried far down the sequence.

The ESLint accessibility rules (angular-eslint’s templateAccessibility ruleset) came in later as a habit rather than an audit. They do not catch the big things – those live in interactions, not markup. But they raise a hand when a label is missing or an aria attribute is wrong, every time you save. Next step is wiring them into CI so a missing label is a build failure rather than a reminder I might ignore.

What clicking and tabbing told me

I set the mouse aside and opened the browser’s developer tools. The accessibility panel has an option to show the page as a full accessibility tree – a nice discovery on its own. The zoom level was rendered as a bare number, which a screen reader would read as “100” with no idea what it was 100 of. The minimap was technically hidden when collapsed, but the underlying image was still in the tree.

Browser DevTools accessibility tree panel showing a text node containing only the number 57, with no surrounding labelThe accessibility tree as a screen reader sees it: “57” with no context.

The keyboard pass was where the audit stopped feeling routine. Tabbing worked fine until I reached the diagram, and at first I thought it worked there too. What I was actually reaching were the expand/collapse buttons on each node. The nodes themselves had no focus, and the plus-buttons for adding new ones only appeared on mouse hover – which meant they did not exist for anyone not using one, and they did not exist for the audit tools either, because there is nothing in the DOM until the pointer event fires. Drag-and-drop reordering was the same story. Two of the most important things in the app were entirely mouse-only, and the previous tools had not flagged any of it. They were not wrong. They were checking what they can check, and they cannot check interactions that only exist while you hover.

What the LLM caught

For the third pass I used Claude with a custom skill based on the accessibility-review skill on MCPMarket. It agreed with the static tools on the obvious things. What it added was findings you can only get by reading the code: the specific input that auto-focused when the properties sidebar opened, the exact pointerdown handler that locked add-buttons to a mouse, and the missing keyboard entry on the diagram as a top finding rather than a footnote. Running it twice gave overlapping but not identical lists. Not every point was worth fixing, and saying no to a few of them ended up being part of the audit too.

Making the diagram keyboard-reachable

The first decision was what kind of thing the diagram actually is. An org chart is a tree, so the canvas got role="tree" and each node got role="treeitem". That changes how a screen reader narrates the structure: instead of “graphic, region” it reads as a hierarchical tree, which matches what the user is actually looking at. Picking the role is the application’s job, not the library’s – ng-diagram does not know whether you are rendering an org chart, a state machine, or a dependency graph, and each of those wants a different role.

The canvas gets the tree role:

<main aria-describedby="diagram-instructions" ...>
  <div role="tree" aria-label="Organization tree">
    <ng-diagram ... />
  </div>
</main>

Each node component declares itself a treeitem through its host bindings, alongside the aria state it needs to expose:

@Component({
  host: {
    '[attr.role]': '"treeitem"',
    '[attr.tabindex]': 'isFocusable() ? 0 : -1',
    '[attr.aria-selected]': 'node().selected',
    '[attr.aria-expanded]': 'ariaExpanded()',
    '[attr.aria-label]': 'ariaLabel()',
  },
})
export class NodeComponent { ... }

isFocusable() is a roving tabindex: only one node is in the tab order at a time, so Tab moves in and out of the canvas in one step rather than walking every node.

The interaction model:

Keys Action
Shift + arrows Move focus between nodes
Arrows (when focused) Move the node on the canvas
Space Expand / collapse children
Enter / Esc Open / close properties panel
Alt + arrows Add a new node next to the focused one (child or sibling depending on layout direction)
Delete Remove the focused node

Animated demo of moving focus between org chart nodes with Shift+arrow keys, expanding a node with Space, and adding a sibling with Alt+arrowKeyboard navigation across the tree: focus moves with Shift+arrows, Space expands, Alt+arrow adds a new node.

Adding all of this raised a question I had not been thinking about: how does the user know any of it exists? Implementing keyboard support is one job. Making sure people can find out it is there is another – a shortcut nobody is told about is a secret.

The skip-link as documentation

The answer ended up sitting next to the skip-link. The skip-link itself jumps focus to the diagram; right after it, a short paragraph lists every shortcut. The diagram’s <main> references that paragraph via aria-describedby, so the instructions get announced when focus enters the canvas. The paragraph stays visible on the page too – sighted keyboard users need to know the shortcuts as much as screen reader users do, and the audit tools cannot tell them.

Top of the page with the focused 'Skip to diagram' link followed by a paragraph listing all keyboard shortcutsThe skip-link with the shortcut list immediately next to it – visible to sighted users, announced to screen readers.

The side effect was the thing I did not see coming. Once there was a place to tell the user about a shortcut, the answer to “the plus-buttons only show on hover” stopped being “show them on focus too” and became “a custom shortcut, written down right next to the skip-link.” Having somewhere to document a shortcut is what made them worth building at all.

The smaller fixes

The static-tool findings mostly translated to small fixes:

  • The page got a <main> landmark and a visually-hidden <h1>.
  • Styled spans pretending to be headings became real heading tags.
  • Icon-only buttons (add, toggle, caret, sidebar, theme, zoom) picked up aria-labels; the toggle-expand button got aria-expanded.
  • “100” in the zoom widget became “Zoom level 100” – the kind of fix where a lot of accessibility work hides: rewriting a number to say what the number is of.
  • Blanket outline: none came out of the form styles; :focus-visible rings went in.
  • The minimap, which duplicates content the keyboard user already navigates on the main canvas, got aria-hidden="true" – along with the button that toggles it, since a control that opens a panel the user cannot perceive is no use to them either.

What I left alone (on purpose)

  • Drag-and-drop hierarchy changes are still mouse-only. A real gap; a keyboard equivalent (cut/paste-style reparenting) was bigger than this round could absorb.
  • No help dialog or shortcut overlay yet. The description next to the skip-link is enough to make the keyboard support discoverable for this pass.
  • The properties sidebar is deliberately non-modal. No focus trap, no focus return on close, because a user editing a node’s properties should still be able to move around the diagram underneath. That one is a feature, not a deferred fix.

Where this leaves me

The audit changed the app. What I did not expect was that it would also change how I look at apps. If I had spent any real time using my own work with the keyboard only, before I started building it, I would have built it differently from day one. That is the thing I am taking forward: not a list of fixes, but the habit of trying the app the way the user will, before the first commit.

Tools like Lighthouse, WAVE, and the LLM pass are useful starting points. They tell you where to look. They cannot tell you whether the app is actually usable. That part still needs a person, a keyboard, and a willingness to be honest with yourself about what you find.

Diagrams and other visual tools have a lot going on – many features, many interactions, many edge cases – and making them work for every user is harder than making a form work for every user. Most of the existing material does not help with the difference. If you are curious about how those problems play out in practice (and where some of them are still unsolved), come talk to us.

Where to look next

GAAD 2026 webinar – Thursday, 21 May, 14:00 CET
Anna Dulny-Leszczyńska and I are hosting Building Accessible Whiteboards and Diagrams: a free, 45-minute talk with real examples instead of theory.

Save your seat

Prompts as Code: How to Version, Test, and Ship the Prompt Layer in 2026

The prompt regression that cost me the worst Sunday of 2025 was twelve characters long.

I had been tuning the system prompt of an LLM feature for about a month. The feature was a structured-output endpoint that took a free-form user message and returned a JSON object with five fields. It worked. Users liked it. Conversion was up. On a Friday afternoon I added what I thought was a small clarification to the prompt: a sentence telling the model to “prefer concise wording” in one of the fields. I deployed. I went into the weekend feeling good about my week.

By Sunday morning, support had three tickets and a Slack DM from our biggest customer. The endpoint was returning malformed JSON on roughly 14% of requests. Not always. Just often enough that retries hid it from our dashboards. The “prefer concise wording” sentence had pushed the model into a mode where it sometimes dropped a comma in the JSON output, because conciseness and valid JSON syntax were apparently in tension in a way I had not anticipated.

The fix was rolling back the prompt. The fix took ninety seconds. The problem was that I had no idea which version of the prompt was running. I had edited the prompt in three places that month: the source code, a draft in Notion, a quick test in the model playground. The deployed version was a copy-paste of one of them. I had no diff. I had no test for “does this prompt still return valid JSON for our golden set of inputs.” I had no rollback button. The Sunday was spent reconstructing what I had typed, retesting it by hand against a list of fifty inputs I dug out of the logs, and deploying the rebuilt prompt.

That weekend is the reason I now treat prompts as code. Not “code-adjacent.” Not “prompts that live in a file in the repo, sort of.” Actual versioned, tested, CI-gated source code with the same review rigor as anything else we ship. The argument for this is not aesthetic. The argument is that you cannot debug what you cannot see, and you cannot roll back what you do not version. Everything else flows from that.

This post is what the prompt-as-code workflow actually looks like, what tools sit at the layer above your editor, and the migration path that gets you from “the prompt is somewhere in a Notion doc” to “the prompt has a CI job and a changelog.”

What Prompts As Code Actually Means

The phrase “prompts as code” gets thrown around like every other piece of AI vocabulary. Let me be specific.

A prompt-as-code workflow has five properties. If any of them are missing, you are doing something else.

Every prompt lives in a tracked file or a tracked registry. Not in a Notion page. Not pasted into a Cloudflare worker. Not in a “system message” field of a hosted dashboard that nobody on the team has the URL for. The prompt has a path. The path is in a repo or in a registry that the repo references by ID. Either is fine. Both are fine. What is not fine is “I think the prompt is in this Slack thread.”

Every change to the prompt is reviewed. Same workflow as a code change. A PR, a review, a merge. The reviewer is allowed to push back. The reviewer is qualified to push back, because the prompt is presented in a diff alongside the test results, not as a 200-line wall of text in a doc with no context.

Every prompt has a test. At minimum, a smoke test that runs the prompt against a small set of golden inputs and validates the output shape. Ideally, an eval that scores quality across a larger set. The test runs in CI before merge. The PR cannot land if the test fails. I went deep on the eval question in the AI evals for solo developers piece; the short version is that even a fifty-row eval set is dramatically better than nothing.

Every prompt is versioned. Not just “the file is in git.” The runtime knows which version is deployed. The logs include the version. When a regression hits, you can look at a request, read the version, and check exactly what prompt was used. This is the single capability that turns prompt debugging from archeology into engineering.

Every prompt is rollbackable. A bad deploy is a one-button revert. Not “redeploy the application with the old prompt re-pasted.” A real revert that flips the active version back to the previous one in seconds, without a full app redeploy, because prompts change more often than code and tying every prompt change to a full app deploy is the friction that turns engineers into copy-paste cowboys.

You can get all five with tools that exist today. The question is just whether your team has actually built the workflow, or whether you are still in the world where the prompt is a string literal in src/services/ai.ts and nobody knows where the canonical version lives.

The Two Architectures That Work

There are two workable architectures for prompts as code in 2026. They are real alternatives, not flavors of the same thing.

Architecture one is prompts in the repo. The prompt files live next to the code that uses them. Format is your call: .prompt files, .md files, TypeScript template literals, YAML. The build system bundles them. The runtime imports them. Version is the git SHA. Rollback is a revert and a redeploy. Tests are colocated with the prompt file.

The strengths are that this is simple, the diff history is obvious, and the same review workflow as code already exists in your team. The weaknesses are that you cannot change a prompt without a full app deploy, the prompt is locked to the application’s release cycle, and non-engineers cannot touch the prompts without going through engineering.

Architecture two is prompts in a registry. The prompts live in a managed service. Langfuse, PromptLayer, Maxim, LangWatch, Helicone, Braintrust. The application fetches the active version at runtime by ID, with optional caching. Updates happen in the registry’s UI or via API. The version is a registry version, not a git SHA. Rollback is a one-click change of the active version. Tests run against the registry, either in CI or in the registry’s own eval system.

The strengths are that prompt updates are decoupled from code deploys, non-engineers can edit prompts safely, and the registry usually includes built-in eval and observability. The weaknesses are that you have introduced a runtime dependency, the prompt history lives outside git so your “source of truth” is now split, and you have to trust the registry to be up when your app is up.

I have shipped both. The choice depends on team shape.

If you are a solo founder or a two-person team, the repo architecture wins. You do not have non-engineers editing prompts. You do not have enough prompts to justify a registry. The simplicity of “the prompt is a file in the repo” is worth more than the flexibility of a registry.

If you are a five-person team with a product manager who wants to A/B test prompt copy, or you have prompts that change daily, or you have prompts you want to roll out to 10% of traffic before everyone, the registry architecture wins. The decoupling pays for itself in week one.

The two are not mutually exclusive. Some teams use registries for the prompts that change often (marketing-style outputs, customer-facing tone) and keep the structural prompts (JSON schemas, agent instructions) in the repo. That hybrid is fine. The rule is: every prompt lives in exactly one place. Never two. Never “it is in the repo but also in the registry and they sometimes disagree.”

The CI Job That Actually Catches Things

The single most valuable artifact in a prompt-as-code workflow is the CI job. Not the registry, not the file format, not the eval framework. The job that runs on every PR and fails the PR if the prompt regression rate exceeds a threshold.

Here is what mine does, in practice. The same shape works for repo-based and registry-based architectures.

The job loads the prompt under review. It loads the eval set, which is a JSON file of input-output pairs that I maintain by hand based on real production examples. The set is around 80 rows for the smaller prompts, 300 rows for the bigger ones. Each row has an input (whatever the prompt is meant to take) and an expected output, or a set of validators the output must pass.

The job runs the prompt against each input. The model behind it is the production model. The runs happen in parallel with a small concurrency limit to avoid rate limits. The cost of a full eval run is between $0.50 and $5 depending on the prompt size and model, which is cheap enough to do on every PR.

The job collects the outputs. It runs the validators. The validators are a mix of structural checks (is this valid JSON, does it have these fields, are the field types correct) and quality checks (is the score from a judge model above this threshold, does this output match the expected pattern). I lean structural where I can, because structural validators are deterministic and judge-based validators are not.

The job emits a pass-rate. The PR fails if the pass-rate drops more than a threshold, currently 3%, from the previous prompt version. The PR also fails if any of a small set of “must-pass” cases regress. Those are the cases I learned about the hard way: the JSON-validity case from the Sunday incident, the SQL-injection case from another bad weekend, the tone-of-voice case from when a customer pointed out that our model had started sounding like a chatbot.

The full CI job is around 200 lines of code. It is the cheapest insurance I have ever bought. In the eight months it has been running, it has caught six regressions that would have shipped without it. Two of them would have been customer-visible incidents. The others would have been silent quality drops that I would have noticed weeks later via support tickets.

If you do not have this job, write it before you do anything else. The exact tool does not matter. The discipline of “no prompt change ships without an eval pass” is the discipline.

The Prompt Diff That Reviewers Actually Read

The other piece nobody talks about is what the PR review actually looks like.

A prompt diff is hard to read. The default GitHub diff shows you the changed lines, but the semantic difference between two prompts is rarely the changed lines. It is what the change does to model behavior. A two-word change can shift outputs significantly. A twenty-line change can have no effect. Reviewing prompts by reading the diff alone is the same as reviewing code by reading the diff without ever running it.

The fix is to attach the eval output to the PR. My CI job posts a comment on every PR with three artifacts. The first is the pass-rate before and after. The second is a sample of 10 outputs from the eval set, side by side: the previous prompt’s output and the new prompt’s output. The third is a list of the failing cases, with input and expected output, so the reviewer can read the actual model behavior that triggered the failure.

The PR review changes shape with that comment in place. The reviewer is not reading the diff and squinting. They are reading the model’s actual behavior, looking at the side-by-side, and deciding whether the change is an improvement or a regression. The diff is supporting evidence. The output is the substance.

This is the same shift that happened in frontend code a decade ago when teams started attaching Percy or Chromatic visual diffs to PRs. The diff is necessary but insufficient. The visual is the actual artifact. Prompts work the same way. The text change is necessary but insufficient. The output behavior is the actual artifact.

If you can get this in place, your team starts having real prompt review conversations. Without it, you are voting on vibes.

What Goes In The Registry vs The Repo

If you go with the registry architecture, the question that consumes the next month of your team’s life is “what goes where.” Here is the heuristic I have ended up with.

Repo: the structural prompts that define agent shape. The JSON schemas. The tool definitions. The system prompts that lock the model into a specific role. Anything where the prompt change is logically a code change because the application’s contract depends on the output shape.

Registry: the surface-level prompts that change with product decisions. The tone of voice. The marketing copy. The user-facing greeting. The prompt that summarizes a document for the user. Anything where the prompt change is logically a product decision and should be touchable by a PM without a code deploy.

The line gets fuzzy. There are prompts that feel structural but actually change often. There are prompts that feel cosmetic but actually have downstream code dependencies. The rule of thumb I use is: if changing the prompt could break a downstream consumer (a JSON schema mismatch, a missing field, a different output type), it goes in the repo. If changing the prompt could only ever affect output quality and tone, it goes in the registry.

Either way, the registry needs to support the same five properties as the repo workflow: tracked, reviewed, tested, versioned, rollbackable. If your registry does not have an audit log, prompt review, eval integration, version pinning, and rollback, you have bought a prettier Notion. Replace it with a real one. The registry market in 2026 is competitive. Tools that lack these features will be irrelevant by 2027.

The Eval Set Is The Hard Part

I want to be honest about which part of this is hard, because the tooling makes it sound easier than it is.

The CI job is a one-week build. The registry integration is a one-day build. The prompt files in the repo are a five-minute decision. None of that is the hard part.

The hard part is the eval set.

A good eval set takes a long time to build, requires real production data, and never finishes. You add cases for every regression you ship. You add cases for every customer report. You add cases when you notice an output that looks weird. The set grows. Six months in, it has 500 rows and you trust it. Twelve months in, it has 1,200 rows and it is one of the most valuable artifacts your company owns. You do not throw it away. You do not regenerate it. You curate it.

Most teams give up on this before month two. The cases feel arbitrary. The pass-rate hovers at 87% and seems impossible to push higher. The eval cost adds up. The temptation is to skip the eval and just deploy.

The teams that push through end up with something rare and valuable: a precise, executable definition of “what our AI feature is supposed to do.” That artifact is more valuable than the model behind it. The model can be swapped for a better one. The eval set tells you whether the swap was actually better, on the specific axes your customers care about. Without the eval set, every model upgrade is a gamble.

If you want a starting point that does not require months of curation, look at the LLM cost optimization workflow I described earlier; the same logs you mine for cost are the logs you mine for eval cases. Pull a hundred recent inputs. Sample for diversity. Write the expected outputs by hand. That is your eval set v0.1. Ship it. Improve from there.

The bar is not perfection. The bar is “better than no eval set.” That bar is low. Clear it and keep moving.

Migration: From “Where Is The Prompt” To Prompts As Code

If you are starting from “the prompt is somewhere in a Notion page and probably also a code file and I am not sure they agree,” here is the migration path that works.

Week one: find every prompt. This is uncomfortable. Most teams discover they have more prompts in production than they thought. Grep the codebase for system:, user:, messages:. Search Slack. Open every Notion page anyone mentions. Make a list. The first time I did this, I found 14 prompts. I had thought we had 6.

Week two: pick one architecture and move one prompt. Do not try to move all 14 in week two. Pick the most-touched prompt. Move it. Build the eval set for it. Wire up the CI job. Watch a PR go through the full workflow. Adjust until it feels right.

Week three: move the next three prompts. Use the workflow you built in week two. Notice what breaks. Improve the tooling.

Months two and three: move the rest. Some prompts will resist. They will be in weird places, owned by people who do not want to touch them, or attached to features you are planning to deprecate. Move them anyway. The cost of one prompt outside the workflow is more than the effort of moving it.

Quarterly: review the eval sets. The same way you review your topical standards files, walk through the eval sets. Cases that are no longer relevant get removed. Cases that catch new regressions get added. The sets stay current. The CI job stays meaningful.

That is the whole migration. It takes a quarter for a small team, half a year for a larger one. The dividend is paid every time you ship a prompt change without a Sunday morning support fire.

What This Changes About How You Build

The part that surprised me most when I made the switch is what it changed about my own decision-making.

Before prompts-as-code, every prompt change felt heavy. I would tweak a word, redeploy, watch the dashboards for an hour, hope nothing broke. The cost of a change was high because the blast radius was unclear. So I changed prompts less often than I should have. The prompts got stale. The feature underperformed. I knew it could be better but I was scared of the breakage.

After prompts-as-code, every prompt change costs the same as a code change. Write the change, open a PR, watch the eval, ship if green. The blast radius is bounded by the eval. The rollback is one click. The fear is gone. So I change prompts more often. The prompts stay current. The feature improves on a normal cadence instead of in stressful sprints.

The same thing happened when teams adopted CI for application code 15 years ago. The cost of a change went down. The volume of changes went up. The quality of the codebase improved because nobody was scared to touch it anymore. Prompts are following the same arc, ten years late.

If your team is still scared of prompt changes, that is the symptom. The fix is the workflow. The discipline of versioning, testing, and rollback removes the fear. The fear was real. The fix is real.

The Sunday incident I started this post with cost me eight hours and a piece of my Sunday. I do not think about that incident often anymore, because the workflow that came out of it has made the kind of mistake I made that day impossible to ship undetected. That is the trade. Eight hours of pain plus a few weekends of building the workflow, in exchange for a system that catches regressions before users do for the rest of your career.

Treat prompts like code. Version them. Test them. Roll them back. The tools exist. The discipline is the gap.

If you are still pasting prompts into Notion in 2026, the cost is not aesthetic. It is the Sunday you have not had yet.

Claude Skills vs Cursor Rules vs Copilot Instructions: How Real Teams Set AI Coding Standards in 2026

The first real fight I had with a teammate over AI tooling was not about which model to use. It was about which config file we were supposed to be editing.

We had a CLAUDE.md at the repo root, written by me back in February. We had a .cursorrules file from the engineer who joined in March, who only used Cursor and refused to even open Claude Code. We had a copilot-instructions.md under .github/, dropped in by whoever turned on Copilot for the org. And we had a half-empty AGENTS.md that someone had heard about at a conference. Four files. Four different versions of the truth. Three subtly different opinions on how to write a database migration. Zero shared understanding.

The bug that triggered the argument was small. A junior engineer asked Cursor to write a Postgres migration. Cursor produced something fine by its own rules. The PR review pointed out that “our convention is to always wrap migrations in a transaction and write a rollback.” Junior engineer pointed at .cursorrules. Nothing about transactions. We checked CLAUDE.md. It had the rule. We checked copilot-instructions.md. Different wording, same intent. We checked AGENTS.md. Empty.

Nobody had written the rule into Cursor’s file because nobody used Cursor when the rule was added. The model did exactly what the file told it to. The file was just wrong by neglect.

This is the new shape of team coding standards in 2026. The standards exist. They just live in five places, in five formats, with five different update schedules. If you are leading any team larger than two people that uses more than one AI coding tool, this post is for you. I am going to walk through what each config file actually does, where the meaningful differences are, and the layout that finally got my team off the Monday argument loop.

The Four Files That Run Your Codebase Now

Before we compare anything, here is the lineup. There are more than four config files in the wild, but these are the ones you actually need to care about.

CLAUDE.md is the Claude Code config. It lives at the root of your repo (or in ~/.claude/CLAUDE.md for personal global rules). Claude Code reads it on every session start and treats the contents as a permanent system prompt. It supports @path/to/file imports up to five levels deep, which means you can split your standards into multiple files and reference them. It also supports auto-memory, where Claude writes notes back into a memory directory based on your corrections and preferences during a session.

.cursorrules (or the newer .cursor/rules/*.mdc directory layout) is the Cursor config. The legacy .cursorrules is a single Markdown file. The new directory format lets you have multiple rules with frontmatter that controls when they apply. The frontmatter supports globs, alwaysApply, and description fields, so you can have a rule that only fires for .tsx files, or only fires when the user asks about migrations. This is genuinely useful and an underused feature on most teams I have seen.

copilot-instructions.md is the GitHub Copilot config. It lives at .github/copilot-instructions.md and applies to Copilot Chat, code review, and the autonomous coding agent. Copilot also supports path-specific instructions under .github/instructions/*.instructions.md with YAML frontmatter that uses glob patterns. The path-specific files combine with the root file when both match, so a TypeScript rule plus a general rule both apply to a .ts file.

AGENTS.md is the closest the ecosystem has to a neutral standard. It started as the OpenAI Codex CLI config, then got adopted by Cursor, Aider, and a handful of smaller tools as a fallback. It supports nothing fancy. No imports, no globs, no path-specific overrides. Just plain Markdown that any compliant agent will read. The widest cross-tool support means it is also the lowest common denominator.

There are others. GEMINI.md for Google Gemini CLI. .windsurfrules for Windsurf. WARP.md for Warp’s agent mode. The pattern is identical to the four above. Pick the ones your team actually uses and ignore the rest.

What Each File Is Actually Good At

Treating these files as interchangeable is the first mistake most teams make. They are not. The features they support are different, and the optimal content for each one is different. Here is what I have ended up using each for.

CLAUDE.md is the longest-lived, most opinionated file in the system. Claude Code’s harness is the most autonomous of the three big tools, which means the rules you write get the most leverage. A single sentence like “always run typecheck before declaring work complete” actually changes behavior across a 40-step agent session. The @import feature lets you split standards into topical files (@.claude/standards/api.md, @.claude/standards/testing.md, @.claude/standards/database.md) and keep the root file as a table of contents. That is how I structure mine now. The auto-memory layer also makes CLAUDE.md grow over time without you babysitting it, which is either a feature or a footgun depending on how much you trust the model’s judgment about what to remember. I cover the broader Claude Code workflow in the agentic coding 2026 piece; the short version is that the leverage on a long-running agent is high enough that CLAUDE.md is worth the most editorial care.

Cursor rules are best treated as path-specific overrides. The new .cursor/rules/*.mdc format with globs is the killer feature. I have a rule that fires only for *.test.ts files and tells the agent to never use mocks for the database layer. I have another that fires only for *.tsx files and tells the agent the design tokens we use. The base file under .cursor/rules/ stays small and general. The path-specific files do the heavy lifting. If you write a 600-line .cursorrules at the root, you are using the tool wrong. Split it.

Copilot instructions are best for repo-wide truths. Copilot’s strength is breadth: every developer with the extension reads it whether they like it or not. Its weakness is that the instructions are short, get truncated more aggressively than CLAUDE.md, and are mostly used for Copilot Chat and the PR-review agent rather than for long autonomous sessions. The right content for copilot-instructions.md is the high-signal rules that you want every developer’s autocomplete and PR-review agent to enforce. Stack choices. Banned patterns. The five things that should never make it into a PR. Not a 200-line essay on architecture.

AGENTS.md is the politeness file. Its job is to exist for the tools that do not read your other files. Treat it as a redirect, not the source of truth. A typical AGENTS.md for my projects is twenty lines: “See CLAUDE.md for full standards. The most important ones are: A, B, C. Run bun test before claiming work is done.” That is enough for an unfamiliar agent to do something reasonable without needing the full CLAUDE.md context.

The mistake teams make is treating all four as parallel. They are not. There is a hierarchy.

The Layout That Finally Worked

After three months of fighting with this, here is the layout my team settled on. It has stayed stable for the last six months, which is the longest any AI tooling decision has lasted in our org.

We have one source of truth: .claude/standards/. Inside it, one file per topic.

.claude/standards/
  api.md
  database.md
  testing.md
  frontend.md
  observability.md
  security.md

Each file is short. 100 to 200 lines. Each one is reviewed by the team lead for that area. Each one has the same structure: a one-paragraph statement of intent, then a list of rules with reasons, then a list of bans with reasons. The structure matters because it forces the writer to justify the rule. A rule with no reason gets removed.

The root CLAUDE.md is a 30-line file that imports the topical files and adds the cross-cutting rules that do not belong to any single domain.

# Project Standards

This project uses Bun, Postgres, Astro, Vercel.

## Topical standards
@.claude/standards/api.md
@.claude/standards/database.md
@.claude/standards/testing.md
@.claude/standards/frontend.md
@.claude/standards/observability.md
@.claude/standards/security.md

## Cross-cutting rules
- Never run destructive SQL without showing the rows first.
- Always run `bun test` and `bun run typecheck` before declaring work complete.
- No em dashes in any file we ship. Ever.

## Workflow
- We use trunk-based development. Feature branches are short-lived.
- PRs are reviewed by the area owner. Big changes get a Linear ticket first.

That is the whole root file. Everything else gets pulled in by import.

The AGENTS.md is fifteen lines and points at the same standards. It is a redirect with a fallback for tools that cannot import.

# Agent Standards

See `.claude/standards/` for full rules. If you cannot read those files,
the four rules you must follow are:

1. Use Bun, not npm or yarn.
2. Never write to the database without showing the rows first.
3. Always run `bun test` and `bun run typecheck` after changes.
4. Follow the existing code style and import order.

Full standards: github.com/our-org/our-repo/tree/main/.claude/standards

The copilot-instructions.md mirrors the same content but compressed for Copilot’s shorter context window. We auto-generate it from the topical files using a small script that runs on pre-commit. That removes the drift problem at the cost of accepting that Copilot reads a summary, not the full standards.

The Cursor rules live in .cursor/rules/ and are tied to globs. The database.mdc rule has globs: ["**/migrations/**", "**/db/**"]. The testing.mdc rule has globs: ["**/*.test.ts", "**/*.spec.ts"]. The frontend.mdc rule has globs: ["**/*.tsx", "**/*.css"]. Each one is the relevant content from the corresponding .claude/standards/ file. We auto-generate these too, from the same source.

The script that does the generation is fifty lines of TypeScript. It reads the topical files, applies a compressor that strips reason-paragraphs and keeps only the rule lines for the shorter formats, and writes the four output files. Pre-commit runs it. The PR review checks that the generated files are in sync. If you edit a generated file by hand, the hook complains.

The point is not that you need exactly this layout. The point is that you need one source of truth and a way to project it into the formats every tool wants to read. Without that, you are running four parallel codebases of rules and one of them will silently drift.

The Things That Actually Cause Drift

Once you understand the file layout, the next problem is keeping the content in sync over time. This is where most teams fail, and the failure mode is always the same.

A new rule gets added to one file and not the others. A senior engineer adds a rule about transaction wrappers to CLAUDE.md after a bad incident. Six months later a junior engineer using Cursor produces the exact code the rule was meant to prevent, because Cursor never saw the rule. The fix is the projection layer above. One source, many outputs. Adding a rule means adding it in one place and letting the script handle the rest.

Old rules are never removed. AI coding standards files have a tendency to grow forever. A rule from 2024 about a deprecated library is still in the file in 2026. The model dutifully follows it. The newer engineer wonders why their code keeps getting rewritten to use Lodash. The fix is quarterly review. One thirty-minute meeting per quarter, the team lead walks down each topical file, and any rule that nobody can defend gets cut. We delete more than we add. The file shrinks roughly 20% per year. That is healthy.

The reasons get lost. A rule that exists without a reason is impossible to refactor. The team cannot evaluate whether the rule still applies, because the original context is gone. The fix is to require a reason for every rule. We use the same structure as my feedback memories in Claude Code: the rule itself, then Why: and How to apply: lines. The reason is mandatory. If you cannot articulate the reason, the rule does not belong in the file.

The standards contradict the codebase. A rule says “always use Tanstack Query for client-side data fetching.” The codebase has 40 SWR calls because it predates the rule. The model sees both and picks one at random. The fix is to either migrate the codebase or update the rule. There is no third option. A standard that contradicts reality is worse than no standard at all, because it teaches the model to ignore standards in general.

Personal preferences sneak into the team file. This one is the hardest. An engineer who hates trailing commas adds “no trailing commas in object literals” to CLAUDE.md. The team has never discussed this. Six months later the codebase is half-formatted that way and the linter has been disabled to accommodate it. The fix is process. New rules go to PR. The team lead reviews. Rules with no team-wide consensus get rejected. The standards file is a team contract, not a personal scratchpad.

The pattern across all four is the same. Drift is a process problem, not a tooling problem. The tools project the rules. The team writes them. If the team is sloppy, no tool will save you.

The Difference Between a Rule and a Skill

There is a deeper distinction that most teams miss, and it is the one I would write into the back of every developer’s hand if I could.

A rule is a constraint. It tells the model what not to do, or what it must do in a specific situation. “Never use mocks in integration tests.” “Always wrap migrations in transactions.” “Use Bun, not npm.” Rules are short. Rules are imperative. Rules belong in CLAUDE.md and friends.

A skill is a procedure. It tells the model how to do a specific task well. “How to add a new API endpoint to this codebase.” “How to review a PR for security issues.” “How to migrate a table without downtime.” Skills are long. Skills are explanatory. Skills belong in the Claude Code plugin marketplace, in SKILL.md files under .claude/skills/, or in the equivalent system for whichever tool you use.

The mistake teams make is putting skills into CLAUDE.md. The file balloons to 800 lines. The model still uses it, but each rule loses leverage because it is buried in three pages of procedural content. Worse, the procedures get triggered every session even when they are not relevant, which wastes tokens and slows everything down.

The right home for a procedure is a skill that fires on demand. Claude Skills, Cursor’s path-globbed rules, GitHub’s path-specific instruction files. Each tool has its own way of saying “load this content when this condition is true.” Use them. Keep the main config file lean. Keep the heavy procedural content in the skill system, where it loads only when needed.

If you read CLAUDE.md files in the wild, you will find this distinction violated everywhere. The 1,200-line CLAUDE.md is almost always 200 lines of rules and 1,000 lines of procedures that should have been skills. Splitting them is a one-day refactor with month-long benefits.

What Each Tool Is Actually Best At, In Practice

After running all three of these tools in production for the better part of a year, here is my honest read on where each one earns its keep. This is also the distribution my team has settled on.

Copilot is the default for everyone. Junior, mid, senior, every laptop in the org. It is the autocomplete baseline. The instructions file gets the most universal rules, the ones we want every PR to enforce. We do not expect Copilot to do agentic work for us. We expect it to make every developer 15% faster on the boring 90% of their day and to catch the obvious mistakes in PR review.

Claude Code is the senior engineer’s tool. It is what we use for the high-leverage agentic work: a multi-file refactor, a migration plan with rollback, a new feature that touches the backend, the frontend, and the database in one shot. The CLAUDE.md and .claude/skills/ are where the senior engineers invest. We do not push Claude Code on the juniors. They graduate into it when they are ready to direct an agent rather than press tab.

Cursor is the IDE-first option for engineers who want a richer editor experience. Some of our team prefers the Cursor UI, some prefers VS Code with Copilot, some lives in Claude Code in the terminal. We do not standardize. The Cursor rules are auto-generated from the same source. The cost of supporting three tools is small once the projection layer exists. The cost of forcing everyone onto one tool is high, because half the team would lose 20% productivity in the switch.

If you compare these tools head-to-head, the deeper comparison lives in the Claude Code vs Cursor vs Copilot 2026 piece. The TL;DR for the config-file question is: each tool needs its config file, the content overlaps 80%, the projection layer is what keeps them honest.

The Workflow For Adding A New Rule

The last thing worth writing down is the workflow itself. Without a workflow, you are back to ad-hoc edits and silent drift.

A new rule starts as a Linear ticket or a Slack thread. Someone notices a recurring problem. Someone writes up the pattern, the proposed rule, and the reason. The team reviews. If there is consensus, the rule lands in the relevant topical file under .claude/standards/ via PR. The PR includes the reason in the commit body. The PR is reviewed by the area owner.

After merge, the projection script runs. The generated files update. Copilot, Cursor, AGENTS.md all reflect the new rule on the next session.

If a new rule contradicts the existing codebase, the PR includes a migration plan. Either the codebase changes to match the rule, or the rule changes to match the codebase. We do not ship a standard that lies about the code.

Every quarter, the team lead reviews each topical file. Rules with no defender are deleted. The file shrinks. The standards stay relevant.

This sounds heavy. It is not. Most quarters the review takes thirty minutes. Most new rules get reviewed in a five-minute Slack thread. The investment is upfront, in building the projection layer and the review habit. Once it is built, it runs itself.

The Part Nobody Talks About

The hidden cost of AI coding standards is not the tooling. It is the social cost of writing them down.

For years, senior engineers got away with carrying the standards in their heads. They reviewed PRs, they corrected the juniors, they shaped the codebase one line of feedback at a time. The standards existed, but they were tacit. The senior was the standard.

The AI tools force tacit standards out into text. Every rule the senior used to hold in their head now has to be articulable, defensible, written. The first time an engineer sits down to write the database.md file for their team, they hit the wall: half of what they “know” is actually preference, not principle. The other half they cannot defend without a long story about an incident from two years ago. Writing the rules surfaces every weak link in your team’s thinking. It is uncomfortable. It is also exactly the thing that makes the team stronger.

The teams I have seen do this well treat the standards file as a team artifact, not a personal one. The senior writes the first draft. The team reviews. The juniors ask why. The senior either has a good answer or removes the rule. After six months, the file is denser and shorter than the first draft, because the only rules that survive are the ones the team can defend.

That is the real prize. Not faster agents. Not more lines of code per day. A team that can explain, to a piece of software, what good looks like. The agents read the file. The juniors read the file. The future senior who joins next year reads the file. The standard is no longer locked in one person’s brain. It is on disk, versioned, reviewable, improvable.

The tools are the surface. The discipline of writing down what you actually believe is the substance.

Pick the layout you can defend. Build the projection script. Run the quarterly review. Stop arguing about which file is the source of truth and start arguing about what the rules should actually be. That is the conversation that makes a team better.

The Monday argument on my team has changed. It is not “which file should we have edited.” It is “should this rule exist at all, and what is the reason.” That is the right argument to be having. If you are not having it yet, that is the goal.

I Built an AI That Calls Real Estate Leads in 30 Seconds — Using MeDo, Retell AI, and Zero Engineers

ags: showdev javascript ai buildinpublic

There’s a stat that’s been stuck in my head for months.

Lead conversion in real estate drops 400% if the first follow-up call happens more than 5 minutes after a buyer submits a form. Most Indian real estate teams call back in 4 to 6 hours.

That gap — between a buyer raising their hand and a human picking up the phone — costs India’s property sector over Rs 15,000 crore every year. Not bad products. Not bad pricing. Just slow follow-up.

So I built PropCall Live. An AI that calls every lead within 30 seconds of form submission. At 11PM. On Sundays. Without a single telecaller.

Here’s exactly how it works and how I built the whole thing without writing code manually.

What happens in 30 seconds
A buyer fills an enquiry form — name, phone, budget, BHK preference, timeline, source.

The moment they hit Submit, three things fire simultaneously:

  1. Lead scoring
    A weighted algorithm calculates a 0–100 score in milliseconds.

Budget fit: 40%
Purchase urgency: 40%
Lead source quality: 20%
A buyer looking for a 3BHK in budget who needs to move immediately scores 94 — HOT 🔥. A casual browser with no timeline scores 32 — COOL.

  1. Telegram notification
    The sales manager’s phone gets a full lead card instantly — name, score, budget, and a personalised Hindi follow-up message generated on the spot.

  2. AI voice call
    Retell AI calls the buyer’s phone. Natural Hindi-English conversation. Qualifies the lead. Books a site visit.

The sales team wakes up to a calendar full of confirmed visits. They never touched a lead manually.

The tech stack

Frontend: React 19 + TypeScript + Tailwind CSS
Voice: Retell AI (Web SDK + phone calling)
Alerts: Telegram Bot API
Weather: Open-Meteo API
Backend: Vercel Serverless Functions
Built with: MeDo AI
How the integrations work
Retell AI — the hard part
Retell’s create-web-call endpoint is designed for server-side use. If you call it directly from the browser you get CORS errors. The fix is a Vercel serverless proxy:

// api/create-call.ts
const RETELL_API_KEY = process.env.RETELL_API_KEY;

export default async function handler(req, res) {
const { agent_id } = req.body;

const upstream = await fetch(“https://api.retellai.com/v2/create-web-call“, {
method: “POST”,
headers: {
Authorization: Bearer ${RETELL_API_KEY},
“Content-Type”: “application/json”,
},
body: JSON.stringify({ agent_id }),
});

const data = await upstream.json();
res.status(upstream.status).json(data);
}
The browser calls /api/create-call. The function proxies to Retell server-side. The API key never touches the client bundle.

The access token comes back, gets handed to the Retell Web SDK:

import { RetellWebClient } from “retell-client-js-sdk”;

const client = new RetellWebClient();
await client.startCall({ accessToken: data.access_token });
Live AI voice call. In the browser. In 30 seconds.

Gotcha I hit: The npm package is retell-client-js-sdk — NOT @retell-ai/retell-client-js-sdk. The scoped package returns a 404 from CDN. Cost me an hour.

Telegram — simpler than you think
Same proxy pattern. The bot token stays server-side:

// api/send-telegram.ts
await fetch(https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage, {
method: “POST”,
headers: { “Content-Type”: “application/json” },
body: JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text: 🔥 NEW LEADnn👤 ${name}n🤖 Score: ${score}/100 — ${label}nn💬 ${hindiMessage},
}),
});
The sales manager sees this on their phone before the AI call has even connected.

Open-Meteo — the detail that impresses
After the form submits, I fetch a 3-day forecast for the project’s coordinates:

const res = await fetch(
“https://api.open-meteo.com/v1/forecast” +
“?latitude=28.5355&longitude=77.3910” +
“&daily=weather_code,temperature_2m_max” +
“&timezone=Asia%2FKolkata&forecast_days=4”
);
WMO weather codes map to visit recommendations — clear sky days get “Good for visit”, rain days get “Not ideal”. Free API, no key required, zero setup.

It’s a small touch. But when a judge or client sees the product suggest “Visit on Thursday — clear sky, 34°C” they understand: this thing is thinking about the whole sales process, not just the form.

Running all three in parallel
The most important line in the entire codebase:

const [telegramSent, callStarted, weatherData] = await Promise.all([
sendTelegram(…),
startRetellCall(),
fetchWeather(),
]);
Three API calls. One await. If you run them sequentially you add 2-3 seconds. Parallel keeps the whole pipeline under 30 seconds.

The MeDo part
I want to be direct about this because it’s the part of the story I find most interesting.

I did not write this code manually. The entire product — React frontend, scoring algorithm, Retell integration, Telegram proxy, weather widget, CRM dashboard, ROI calculator, Vercel serverless functions, security architecture — was built by describing what I wanted to MeDo in plain English.

Not “generate a component.” Full architectural decisions. “The Retell API key should never be in the browser — how do we proxy it?” MeDo figured out the serverless function approach, implemented it correctly, and caught the CORS issue before I’d even thought to look for it.

The traditional path to this product: 2-3 engineers, 3 months, Rs 15-20 lakhs. The actual path: one person with a clear product vision and MeDo.

I don’t say this to dismiss engineering. I say this because if you have a product idea and the only thing stopping you is “I can’t build it” — that excuse is gone.

What I’d do differently
Security first. I started with API keys in constants.ts in the client bundle. Moving them to environment variables and server-side proxies later meant touching five files. Starting with the proxy architecture would have been faster.

Verify CDN URLs before shipping. I had a broken CDN script loading a 404 in production for longer than I’d like to admit. Always curl the URL before you trust it.

The follow-up speed slider matters. The ROI calculator had a “current follow-up speed” slider that did nothing in the calculation for a while. Wiring it to the lost-leads formula — faster follow-up means fewer leads lost, with a real curve — made the tool honest.

What’s next
Outbound phone calls to actual mobile numbers (not just browser calls)
White-label self-serve onboarding for any developer
Tamil, Marathi, Telugu agents
CRM webhook integrations (Kylas, Sell.Do)
If you’re building anything with Retell AI or want to talk about AI-native product development in India — I’m at [your contact].

Live demo: [your Vercel URL]
GitHub: https://github.com/arpit2222/propcall

“Complexity Ceiling” in Power Apps

Power Apps is a powerhouse for rapid prototyping within the Microsoft ecosystem, but for complex development, the “seams” are becoming visible. This new analysis explores the “Complexity Ceiling” and the friction points faced by architects and developers.

Core Issues Identified:

  • Mental Model Mismatch: Chaining nested With blocks to simulate sequential logic.
  • Tooling Gaps: The need for real breakpoints and execution narratives over simple trace functions.
  • Governance Hurdles: The “abrupt cliff” of enterprise compliance for makers.

Microsoft has the opportunity to bridge the gap between business analysts and professional engineers by introducing controlled imperative constructs and better code organization.

https://www.alanbonnici.com/2026/05/the-complexity-ceiling-where-microsoft.html

Gemini Nano On-Device Function Calling for Android

---
title: "Gemini Nano Function Calling: Building Offline AI Agents on Android"
published: true
description: "A hands-on guide to architecting offline AI agents on Android using Gemini Nano's on-device function calling, structured JSON output, and a WorkManager + Room sync pipeline."
tags: android, kotlin, architecture, mobile
canonical_url: https://blog.mvpfactory.co/gemini-nano-function-calling-offline-ai-agents-android
---

## What We Will Build

Let me show you a pattern I use in every project that needs on-device intelligence. We are going to architect an offline-capable AI agent on Android using Gemini Nano's function calling and structured JSON output — the features Google expanded at I/O 2026. By the end, you will have a validation pipeline that tames on-device hallucinations and a WorkManager + Room queue that executes agent actions when connectivity returns.

## Prerequisites

- Android Studio with Kotlin
- Familiarity with Room and WorkManager
- Access to Gemini Nano on-device APIs (AI Core)
- A device or emulator supporting on-device inference

## Step 1: Respect the 32K Token Budget

Cloud Gemini Flash gives you 1M+ tokens. Gemini Nano gives you roughly 32K on-device. That budget covers your system prompt, tool definitions, conversation history, *and* the response. Most teams get this wrong by porting cloud schemas directly.

Here is the minimal setup to get this working:

kotlin
// Bad: verbose schema that eats your token budget
val cloudSchema = Tool(
name = “create_calendar_event”,
description = “Creates a new calendar event with the specified title, ” +
“date, time, duration, location, attendees, recurrence pattern, ” +
“reminder settings, and optional notes…”,
parameters = /* 12 parameters with long descriptions */
)

// Good: minimal schema optimized for on-device budget
val nanoSchema = Tool(
name = “cal_create”,
description = “Create event”,
parameters = listOf(
Param(“title”, “string”, required = true),
Param(“iso_time”, “string”, required = true),
Param(“dur_min”, “int”, required = true)
)
)


A trimmed schema set of 5 tools consumes roughly 800–1,200 tokens, leaving headroom for conversation context. A verbose 15-tool schema can eat 4,000+ tokens before a single user message. Budget 1,200 tokens maximum for tool definitions. Use short names, minimal descriptions, and cap at 5 tools per agent context. Swap tool sets dynamically based on user intent rather than loading everything at once.

## Step 2: Build a Three-Layer Validation Pipeline

A quantized on-device model hallucinates more than its cloud counterpart. For function calling, this shows up as malformed JSON, invented parameter names, or calls to tools that do not exist in your schema.

The docs do not mention this, but Layer 1 alone catches roughly half of all failures — the model returns valid function calls but wraps them in explanatory text.

kotlin
fun parseAgentAction(raw: String): AgentAction? {
// Layer 1: Extract JSON from response (model may wrap it in markdown)
val json = JsonExtractor.findFirst(raw) ?: return null

// Layer 2: Validate against registered tool schemas
val parsed = try {
    toolRegistry.parse(json)
} catch (e: SchemaValidationException) {
    null
}

// Layer 3: Semantic bounds checking
return parsed?.takeIf { action ->
    semanticValidator.isReasonable(action)
    // e.g., duration_min in 1..480, title.length < 200
}

}


## Step 3: Wire Up the WorkManager + Room Offline Queue

Where on-device function calling really earns its keep is offline operation. A user on an airplane says "schedule a team sync for Tuesday at 2pm." Gemini Nano parses the intent locally, but the calendar API requires connectivity.

kotlin
@entity(tableName = “agent_actions”)
data class AgentAction(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val toolName: String,
val paramsJson: String,
val status: ActionStatus = ActionStatus.PENDING,
val createdAt: Long = System.currentTimeMillis()
)

val request = OneTimeWorkRequestBuilder()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setInputData(workDataOf(“action_id” to action.id))
.build()

WorkManager.getInstance(context).enqueue(request)


Gemini Nano produces a structured `AgentAction`. Room persists it with status `PENDING`. WorkManager enqueues a `OneTimeWorkRequest` with network constraints. An executor processes the action when connectivity returns, updating status to `COMPLETED` or `FAILED`. You get immediate user feedback ("Got it, I'll create that event when you're back online") while guaranteeing eventual execution. Room provides durability across process death, and WorkManager handles retry with exponential backoff.

| Dimension | Gemini Nano (on-device) | Gemini Flash (cloud) |
|---|---|---|
| Context window | ~32K tokens | 1M+ tokens |
| Latency (first token) | 80–200ms | 300–800ms (network dependent) |
| Function call reliability | Degrades with schema complexity | Stable across complex schemas |
| Structured JSON consistency | Requires validation + retry | Generally reliable |
| Availability | Always-on, no network needed | Requires connectivity |
| Cost per call | Zero marginal | Per-token API pricing |

## Gotchas

- **Token budget blowout.** Porting your cloud tool schemas directly to Nano will silently consume your context window. You will get incoherent responses with zero error messages. Keep schemas under 1,200 tokens total.
- **Markdown-wrapped JSON.** The model frequently wraps valid JSON in explanatory text or markdown code fences. A solid JSON extractor is table stakes — without one, you will reject roughly half of perfectly good responses.
- **Invented parameters.** Nano will hallucinate parameter names that do not exist in your schema. Always validate against your registered tool definitions before executing anything.
- **Skipping the offline queue.** Adopt the WorkManager + Room pattern early, even if your initial use case is online-only. This architecture lets you go offline with zero refactoring. The persistence layer also doubles as an audit log of every agent action — useful for debugging and showing users what the agent did on their behalf.

## Wrapping Up

The latency advantage of on-device inference (80–200ms vs 300–800ms) matters for interactive mobile UX. But the reliability gap is the architectural challenge you actually need to design around. Shrink your schemas, validate in layers, and queue actions with Room + WorkManager. That combination turns Gemini Nano from a demo into a production-grade offline agent pipeline.

I Deleted 40% of Our Kubernetes Config. Here’s What Stayed.

I Deleted 40% of Our Kubernetes Config. Here’s What Stayed.

Our Kubernetes manifests were 2,000 lines of copy-pasted YAML. After cleanup: 1,200 lines. Better reliability.

What I Removed

Duplicate HPA configurations that contradicted each other. Init containers that did nothing. Resource limits set by guessing. Multiple replicas for stateless services that’s pointless.

What Stayed

The actual health checks (liveness and readiness probes that matched real behavior). Proper pod disruption budgets. The services that actually needed ClusterIP vs the ones we changed to LoadBalancer out of habit.

The Process

  1. Delete one thing
  2. Deploy to staging
  3. Watch for 24 hours
  4. If nothing breaks, merge

Sounds tedious. It is. But we caught three real issues during the process.

The Surprising Finding

Most of our “production-grade” config was cargo-culted from tutorials. Real production needs are usually simpler than tutorials suggest.

What I’d Tell My Past Self

Start with minimal configs. Add complexity only when you have a specific reason, not anticipated future needs.

What’s your Kubernetes config-to-code ratio?

Building a Safe Internal AI Assistant with Amazon Kendra and Amazon Bedrock

AWS Data Security

AWS Data Security

A practical, human guide for teams trying to reduce risky copy/paste into external AI tools

Let’s start with the real problem.

Most teams are not using ChatGPT, Claude, Midjourney, Canva, or other AI tools because they want to break security policy. They use them because they are busy, under pressure, and trying to get work done.

A developer needs help with an error message.

A security engineer needs the latest data-handling rule.

An HR or IT team member needs the right internal process.

A project manager needs to understand which AWS account belongs to which client.

The answer probably exists somewhere already. It may be in Confluence, Google Drive, Slack, an AWS runbook, or an old project folder.

But if finding the answer internally takes 20 minutes and an external AI tool gives a useful answer in 20 seconds, people will naturally choose speed.

That is the real security problem we are trying to solve together.

Not: “How do we stop people from using AI?”

The better question is:

How do we give employees a safe, approved, useful AI assistant that helps them work faster without leaking internal data?

That is where Amazon Kendra, Amazon Bedrock, and a properly designed Retrieval-Augmented Generation (RAG) architecture can help.

The environment we are solving for

This blog is written for an organization that looks like this:

  • Confluence stores security, IT, business, HR, policy, procedure, and development-environment design documents.
  • Google Drive is used for file sharing and cloud storage.
  • Google Workspace is the identity provider and SSO platform.
  • AWS has multiple accounts for different clients, projects, and environments.
  • Slack is used heavily for team messaging.
  • Employees use AI tools every day for coding, troubleshooting, writing, design, research, and operations.
  • External AI platforms are already in use, including ChatGPT, Claude, Gemini, Midjourney, Canva, and others.
  • There are limited guardrails today to prevent users from pasting sensitive internal data into those tools.

This is a common situation.

It does not mean the organization is careless. It usually means AI adoption has moved faster than governance, security tooling, and internal knowledge management.

So our job is to design something practical.

We need a solution that helps users, protects data, supports audit needs, and does not create so much friction that everyone works around it.

The problem in plain English

The organization has valuable knowledge, but it is scattered.

Some of it is in Confluence.

Some of it is in Google Drive.

Some of it is buried in Slack.

Some of it is tied to AWS accounts, client projects, runbooks, and architecture decisions.

When people cannot find the right answer quickly, they start doing this:

“I will just paste the policy, error message, runbook, or architecture snippet into ChatGPT and ask for help.”

That one action creates multiple risks:

  • internal policies may leave the organization;
  • client or project information may be exposed;
  • AWS architecture details may be shared with an unapproved vendor;
  • source code or secrets may be pasted by mistake;
  • HR, legal, or incident information may be disclosed;
  • the security team may have no audit trail;
  • the organization may breach contractual, regulatory, or privacy obligations.

This is why the answer cannot be only “write a policy.”

A policy helps, but people still need a better way to work.

The safer pattern is:

Give users an internal AI assistant that can answer from approved internal sources, respect permissions, use Google identity, log safely, and apply guardrails before sensitive content leaves the trusted environment.

What is RAG?

RAG stands for Retrieval-Augmented Generation.

That sounds technical, but the idea is simple.

A normal AI chatbot answers from what the model already knows or from whatever the user pastes into the chat.

A RAG assistant does something safer and more useful:

  1. The user asks a question.
  2. The system checks who the user is.
  3. The system searches approved internal sources.
  4. It retrieves only the content the user is allowed to access.
  5. It sends only the relevant excerpts to the AI model.
  6. The AI model writes an answer using that retrieved content.
  7. The answer includes sources where possible.
  8. The event is logged for security monitoring and audit.

The important point is this:

In a secure RAG design, the model is not the source of truth. Your approved internal documents are the source of truth.

That matters because we do not want the AI assistant inventing policy, guessing approvals, or exposing documents the user should not see.

What is Amazon Kendra?

Amazon Kendra is AWS’s managed enterprise search service.

For this design, think of Kendra as the search and retrieval layer.

It connects to approved repositories, indexes content, and returns the most relevant passages when a user asks a question.

In our scenario, Kendra can help search:

  • Confluence spaces;
  • Google Drive shared drives and approved folders;
  • selected Slack channels, if approved;
  • S3 buckets that contain approved AWS runbooks, architecture records, policies, or compliance documents.

Kendra is useful because it can support user-aware retrieval. In simple terms, it can help make sure users only receive search results they are allowed to see.

That is a big deal.

Without this, the AI assistant could become a very fast data leakage engine.

With this, the assistant can become a safer front door to internal knowledge.

But there is one rule we should be strict about:

Kendra must not become a dumping ground for every document in the company.

Indexing needs ownership, approval, classification, and access-control testing.

What is Amazon Bedrock?

Amazon Bedrock is AWS’s managed service for building generative AI applications with foundation models.

In this design, Bedrock is the answer-generation layer.

Kendra finds the relevant internal content. Bedrock turns that content into a readable answer.

A secure Bedrock setup should include:

  • a system prompt that tells the model to answer only from retrieved sources;
  • Bedrock Guardrails for sensitive data, prompt attacks, denied topics, and unsafe outputs;
  • refusal behavior when the answer is not available from approved content;
  • source references so users can verify the answer;
  • low-temperature settings for policy, compliance, and operational answers.

The model should not receive entire document libraries.

It should receive the smallest useful set of authorized excerpts needed to answer the user’s question.

That is how we reduce exposure while still helping the user.

What this solution can and cannot do

This part is important.

Amazon Kendra and Amazon Bedrock can help us build a safe internal AI assistant.

They can help employees stop pasting internal data into unmanaged AI tools because they now have a useful approved alternative.

But they do not automatically control what a user types into ChatGPT, Claude, Midjourney, Canva, or another external AI platform.

So the complete solution has two parts.

Part 1: Give users a safe internal AI assistant

This is the Kendra + Bedrock RAG platform.

It should be the preferred place to ask questions about internal policies, procedures, AWS runbooks, development-environment designs, and approved operational guidance.

Part 2: Control risky external AI usage

This requires security controls outside Kendra and Bedrock, such as:

  • an AI acceptable-use policy;
  • data classification;
  • CASB;
  • Secure Web Gateway;
  • DLP;
  • endpoint controls;
  • secure browser controls;
  • an approved AI vendor register;
  • legal and privacy review;
  • an exception process;
  • SIEM monitoring.

If we only build the internal assistant but do not manage external AI usage, the risk remains.

If we only block external AI but do not give users a good alternative, people will look for workarounds.

The balanced answer is to do both.

The target architecture

Here is the clean version of what we are building.

Employee
  |
  | Google SSO
  v
Internal AI Portal or Slack Bot
  |
  v
API Gateway
  |
  v
RAG Backend
  |
  |-- Validate Google identity
  |-- Resolve groups from a trusted source
  |-- Check prompt for secrets or restricted content
  |-- Apply data-handling policy
  |
  v
Amazon Kendra
  |
  |-- Confluence connector
  |-- Google Drive connector
  |-- Optional Slack connector
  |-- Optional S3 approved knowledge source
  |-- ACL and user-context filtering
  |
  v
Authorized excerpts only
  |
  v
Amazon Bedrock + Bedrock Guardrails
  |
  v
Grounded answer with sources
  |
  v
User

Security telemetry should flow to the security team:

API Gateway logs
Lambda application logs
CloudTrail
CloudWatch
Kendra admin/query events
Bedrock Guardrail events
CASB/SWG/DLP events
SIEM/SOAR

External AI usage needs a separate control path:

User -> External AI Platform
        |
        v
CASB / SWG / DLP / Secure Browser / Endpoint Control
        |
        |-- Allow low-risk approved use
        |-- Warn the user
        |-- Block restricted data upload
        |-- Log the event
        |-- Route exception requests

This gives us a practical model:

  • help users with internal AI;
  • reduce risky copy/paste;
  • enforce permissions;
  • monitor misuse;
  • preserve audit evidence.

Step 1: Start with the use cases, not the technology

This is where many AI projects go wrong.

They start by asking:

“Which model should we use?”

That is not the first question.

The better first question is:

“Which user problems are we solving safely?”

Good first use cases are:

  • “Where is the vendor data-sharing procedure?”
  • “What is the approved process for creating a new AWS account?”
  • “Which security standard applies to development environments?”
  • “What is the incident response process for suspected data leakage?”
  • “What is the approved way to share files with a client?”
  • “Which Confluence page explains our developer onboarding process?”
  • “Which AWS guardrails apply to client project accounts?”

These are valuable, common, and manageable.

Avoid starting with:

  • full Slack workspace search;
  • full HR file search;
  • legal folders;
  • finance exports;
  • customer data exports;
  • source-code repositories;
  • incident evidence;
  • production secrets;
  • all Google Drive content;
  • all Confluence spaces.

We are not trying to prove that the assistant can read everything.

We are proving that it can safely answer useful questions.

Step 2: Classify the data before indexing it

Before connecting Kendra to repositories, agree on a simple classification model.

Classification Example AI handling
Public Published marketing content Allowed in approved tools
Internal General internal procedures Allowed in internal RAG
Confidential Security designs, client/project documents Internal RAG only with ACL enforcement
Restricted Credentials, sensitive customer data, HR/legal/incident records Do not index unless explicitly approved

This classification does not need to be perfect on day one.

But it does need to be clear enough to stop unsafe indexing.

A good rule is:

If we would be uncomfortable seeing the content summarized in an AI answer, we should not index it until the owner, access model, and guardrails are ready.

Kendra metadata should include classification where possible.

The backend should also apply a second check before sending retrieved content to Bedrock.

That gives us defense in depth.

Step 3: Use Google identity properly

Google Workspace is already the identity provider, so we should use it.

But we need to avoid a common mistake.

A Google ID token can prove who the user is, but it may not contain all the group membership information needed for authorization.

So the RAG backend should not simply trust group names sent by the browser.

Better options are:

  1. Use an internal identity broker that validates Google SSO and issues signed application claims.
  2. Resolve group membership server-side using Google Cloud Identity or Directory APIs.
  3. Use AWS IAM Identity Center integrated with Google Workspace, if that fits your identity strategy.
  4. Maintain a controlled mapping between Google groups and Kendra filters.

The goal is simple:

The user should only retrieve documents they are already allowed to access in the source system.

If the assistant gives a user more access than Confluence or Google Drive would give them directly, the design has failed.

Step 4: Decide how to separate clients, projects, and AWS accounts

This matters a lot in multi-account AWS environments.

If your organization has separate AWS accounts for different clients or projects, your knowledge base should respect that separation.

There are three common patterns.

Option A: One central Kendra index

This is operationally simpler, but it requires mature ACLs and metadata.

Use it only when all content belongs to the same organization and cross-project leakage is not a strict contractual concern.

Option B: Separate Kendra index per client or project

This is usually better for consulting, MSP, MSSP, or project-based environments.

It reduces the risk of one client’s information appearing in another client’s answer.

Option C: Separate AWS account per client or project RAG environment

This is the strongest isolation model.

Use this when contracts, regulations, or customer commitments require strict separation.

For most organizations handling client-sensitive information, Option B or C is safer.

The operating principle is:

The RAG architecture should follow the same isolation model as the business and cloud environment.

Step 5: Connect Confluence carefully

Confluence is probably the best first source.

It usually contains policies, procedures, runbooks, architecture notes, and development-environment designs.

But do not connect all of Confluence at once.

Start like this:

  1. Pick one or two approved spaces.
  2. Assign a data owner for each space.
  3. Review permissions.
  4. Remove stale broad-access groups.
  5. Exclude test, archive, personal, and unrestricted spaces.
  6. Configure the Kendra Confluence connector.
  7. Enable ACL ingestion where supported.
  8. Sync the data source.
  9. Test access with users from different roles.
  10. Review what the assistant returns.

Use positive and negative tests.

Test Expected behavior
Security engineer asks for a security runbook they can access Answer returned with source
Developer asks for a restricted incident report No restricted source returned
HR user asks for development architecture Only authorized content returned
User asks about a policy outside approved spaces Assistant says it does not have enough approved context

Do not skip negative testing.

That is how you catch overexposure before users do.

Step 6: Connect Google Drive with extra caution

Google Drive is powerful, but permissions can be messy.

There may be shared links, inherited permissions, old project folders, personal files, externally shared files, and forgotten documents.

Start with Shared Drives, not every user’s My Drive.

Good first sources:

  • approved IT procedures;
  • approved security standards;
  • developer onboarding guides;
  • cloud architecture templates;
  • approved compliance summary documents;
  • non-sensitive AWS runbooks.

Avoid at the beginning:

  • personal My Drive content;
  • HR case folders;
  • legal folders;
  • finance exports;
  • raw customer exports;
  • incident evidence folders;
  • unreviewed client directories.

The checklist is simple:

  1. Identify the folder owner.
  2. Review external sharing.
  3. Remove broad link-based access where it is not needed.
  4. Configure the Kendra Google Drive connector.
  5. Use inclusion and exclusion rules.
  6. Validate document-level permissions.
  7. Test with users from different groups.
  8. Review logs and returned sources.

If Google Drive is not cleaned up before indexing, the assistant may expose historical permission mistakes faster than normal search ever did.

That is why we index slowly and test carefully.

Step 7: Treat Slack as a front end first, not a data source

Slack is useful, but it is risky to index.

It contains informal decisions, screenshots, troubleshooting notes, incident discussions, old opinions, pasted logs, and sometimes secrets.

So our recommended approach is:

Use Slack as a way to ask the assistant before using Slack as a source of truth.

A safer pattern looks like this:

  1. User asks the Slack bot a question.
  2. The Slack bot maps the Slack user to Google Workspace identity.
  3. The bot calls the internal RAG API.
  4. The API applies the same identity, Kendra, and Bedrock controls.
  5. The answer is returned as an ephemeral message or direct response.
  6. Sensitive answers are not posted into shared channels.

Only index Slack later, and only after legal, privacy, and data owners approve it.

If Slack indexing is approved, start with a small number of knowledge channels.

Do not index DMs by default.

Do not index all private channels by default.

Do not index incident channels without explicit approval.

Step 8: Add AWS knowledge through approved documents

The assistant does not need direct access to every AWS account.

That would create unnecessary risk.

Instead, publish approved AWS knowledge into Confluence, Google Drive, or S3.

Useful content includes:

  • AWS account inventory;
  • client/project ownership matrix;
  • landing zone standards;
  • SCP and guardrail documentation;
  • cloud deployment process;
  • incident response runbooks;
  • Security Hub, GuardDuty, Macie, and CloudTrail operating procedures;
  • WAF and CloudFront standards;
  • approved architecture decision records;
  • data classification by account or project.

This gives engineers the answers they need without giving the assistant broad live access to cloud environments.

For client or project separation, use:

  • separate indexes where needed;
  • metadata filters;
  • Google group mapping;
  • document ownership;
  • quarterly access reviews;
  • cross-project query monitoring.

Step 9: Add Bedrock Guardrails and application guardrails

Do not rely only on the model prompt.

Prompts are useful, but they are not enough for production security.

Use Bedrock Guardrails and application checks together.

Guardrails should cover:

  • prompt-injection attempts;
  • requests for secrets;
  • access keys, tokens, passwords, and private keys;
  • requests to bypass policy;
  • requests to exfiltrate data;
  • unsafe coding or operational instructions;
  • regulated personal data where blocking or masking is required;
  • unsupported answers where retrieved context is insufficient.

The application should also enforce rules such as:

Answer only from retrieved authorized sources.
If the source context is insufficient, say so.
Do not invent policy.
Do not infer approval.
Do not reveal secrets.
Do not summarize restricted content unless explicitly allowed.
Cite sources where possible.

This protects the user too.

A good assistant should not give a confident but wrong answer.

For security, compliance, and operations, “I do not have enough approved context” is often the safest answer.

Step 10: Log safely

Security teams need visibility.

But logging everything is dangerous.

User questions may contain secrets, customer names, source code, incident details, or HR information.

Model answers may contain summarized confidential content.

Retrieved excerpts may contain restricted policy or architecture information.

So the production logging rule should be:

Log enough to investigate misuse, but not enough to create a second sensitive data repository.

Good fields to log:

  • hashed user ID;
  • timestamp;
  • request ID;
  • source application;
  • Kendra query ID;
  • number of retrieved passages;
  • classification counts;
  • guardrail decision;
  • block reason;
  • latency;
  • error code;
  • client/project metadata where safe.

Avoid logging by default:

  • raw user query;
  • full prompt;
  • retrieved excerpts;
  • model answer;
  • document body;
  • secrets or detected sensitive values.

This is one of the most important production controls.

Otherwise, the AI logging pipeline becomes its own data leakage risk.

Step 11: Control external AI platforms without making users the enemy

This is where the tone matters in real life.

Users are not the enemy.

Most risky AI behavior happens because users are trying to move fast and do the right thing with poor tools.

So the control strategy should feel fair:

  1. Give users a good internal assistant.
  2. Explain what data can and cannot go into external AI tools.
  3. Allow approved external AI tools for public or low-risk work.
  4. Block or warn when confidential or restricted data is pasted externally.
  5. Provide a quick exception process.
  6. Coach repeat offenders instead of only punishing them.
  7. Use SIEM reporting to find patterns and improve guidance.

A simple policy model:

Data type External AI Internal RAG
Public Allowed in approved tools Allowed
Internal Allowed only in approved enterprise AI tools Allowed
Confidential Not allowed in unmanaged AI tools Allowed with ACLs
Restricted Not allowed Only with explicit approval or not indexed

Technical controls may include:

  • CASB;
  • Secure Web Gateway;
  • DLP;
  • endpoint DLP;
  • secure browser;
  • browser extension control;
  • DNS/web filtering;
  • SaaS allowlist/blocklist;
  • enterprise AI vendor controls.

The goal is not to block innovation.

The goal is to make the safe path easier than the risky path.

Step 12: Monitor for misuse and control failure

The SOC should not monitor every question like a surveillance program.

But it should monitor meaningful risk signals.

Examples:

Signal Why it matters
Repeated blocked prompts User may be pasting secrets or restricted data
High query volume by one user Possible scraping or compromised account
Queries across many client names Possible reconnaissance or cross-client harvesting
Kendra ACL sync failures Could cause overexposure or missing access
New broad data source added Could expand searchable content unexpectedly
Bedrock Guardrail blocks Indicates policy or safety issues
External AI DLP blocks Indicates attempted sensitive upload
Slack bot used in sensitive channels May expose answers to the wrong audience

Example detection ideas:

IF a user submits more than 25 RAG queries in 10 minutes
AND queries reference more than 3 client or project names
THEN create a SOC alert for possible internal data harvesting.
IF prompt DLP detects an access key, private key, password, or token
THEN block the request, show safe guidance, and create a security event.
IF CASB/SWG blocks upload to an external AI domain
AND content classification is Confidential or Restricted
THEN create a DLP case and notify the data owner.

The response should be proportionate.

Not every blocked prompt is malicious.

Sometimes the control worked, and the user simply needs guidance.

Step 13: Roll out in phases

A safe rollout is better than a big launch.

Use this sequence:

  1. Approve the first AI use cases.
  2. Define data classification and AI-handling rules.
  3. Create a dedicated AWS security AI account.
  4. Enable Bedrock model access.
  5. Create Bedrock Guardrails.
  6. Create the Kendra index.
  7. Connect one low-risk Confluence space.
  8. Validate ACL filtering.
  9. Deploy the RAG API.
  10. Connect an internal web portal.
  11. Add one approved Google Shared Drive.
  12. Validate Google identity and group mapping.
  13. Add Slack as a front end.
  14. Consider Slack indexing only after legal and privacy approval.
  15. Integrate logs with the SIEM.
  16. Pilot with security, IT, and engineering users.
  17. Expand by data source and use case.

At each phase, ask:

  • Did the assistant answer useful questions?
  • Did it refuse when it should?
  • Did it respect document permissions?
  • Did the logs avoid sensitive content?
  • Did users trust the answers?
  • Did the tool reduce external AI copy/paste?

That last question matters.

The goal is not just to deploy architecture.

The goal is to change behavior safely.

What a good answer looks like

Imagine a user asks:

Can I send customer data from the analytics platform to an external dashboard vendor?

A good answer would say something like:

Based on the approved data-handling and vendor-sharing procedures available to you, customer data may only be shared with an external vendor after data-owner approval, vendor risk review, an approved transfer method, and required contractual or privacy checks. I do not have an authorized source confirming that this specific dashboard vendor is approved for the analytics platform. Please submit the request through the approved vendor data-sharing workflow.

That answer helps the user.

It does not shame them.

It does not invent approval.

It points them to the right process.

An unsafe answer would be:

Yes, export the data and upload it to the vendor dashboard.

Another unsafe answer would be:

The restricted architecture diagram says the analytics platform contains these customer fields…

If the user is not authorized to see the restricted diagram, the assistant must not reveal or summarize it.

Implementation checklist

Identity

  • Google SSO is enabled.
  • MFA and conditional access are enforced where required.
  • Groups are resolved server-side.
  • Browser-supplied groups are not trusted.
  • Group-to-Kendra mapping is tested.
  • Privileged access is reviewed.

Kendra

  • The Confluence connector is scoped to approved spaces.
  • The Google Drive connector is scoped to approved Shared Drives or folders.
  • The Slack connector is optional and approved.
  • ACL ingestion is validated.
  • Public or no-ACL documents are reviewed.
  • Classification metadata is applied where practical.
  • Access tests include both allowed and denied users.

Bedrock

  • An approved model is selected.
  • A Bedrock Guardrail is configured and versioned.
  • Prompt-attack filtering is enabled.
  • Sensitive information filters are enabled.
  • Refusal behavior is tested.
  • Citations or source references are returned where possible.

AWS platform

  • A dedicated security AI AWS account is used.
  • IAM least privilege is applied.
  • KMS encryption is configured.
  • CloudTrail is enabled.
  • CloudWatch log retention is set.
  • Logs are forwarded to the SIEM.
  • API Gateway and Lambda do not log raw prompts by default.
  • WAF is used if the API is internet-exposed.

External AI governance

  • An AI acceptable-use standard is published.
  • An approved AI tools register is maintained.
  • CASB/SWG/DLP controls are enabled.
  • An exception workflow is defined.
  • User guidance includes safe and unsafe examples.
  • Violations are monitored and handled proportionately.

The honest conclusion

Yes, this design solves a real problem.

But only if we position it correctly.

Amazon Kendra and Amazon Bedrock are not magic controls that stop every external AI risk.

They are the foundation for a better internal option.

The real solution is the combination of:

  • an approved internal RAG assistant;
  • Google SSO and trusted group resolution;
  • Kendra ACL-aware retrieval;
  • Bedrock generation with guardrails;
  • safe logging;
  • data classification;
  • client/project isolation;
  • SIEM monitoring;
  • DLP/CASB/SWG controls for external AI;
  • clear policy and user education.

The human lesson is simple:

People will use the tool that helps them get work done. Security’s job is to make the safe tool useful enough that people choose it naturally.

That is how we reduce shadow AI.

That is how we protect internal knowledge.

And that is how we give employees the speed of AI without asking them to gamble with company, client, or personal data.