Which index should SQLite use?

git-lrc

Hello, I’m Maneshwar. I’m building git-lrc, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product.

Even when indexes exist, choosing the wrong one can slow down a query significantly.

The optimizer’s job here is not just to use an index, but to use the right index.

One Table, One Index (Mostly)

For each table in a query, SQLite can typically use only one index.

There is one exception.

In OR-based queries, SQLite may use multiple indexes, but in most cases, it selects a single index per table.

Because of this limitation, index selection becomes a critical decision.

SQLite tries to ensure that at least one useful index is applied to each table whenever possible.

When Multiple Indexes Exist

Consider a table with multiple indexes:

“`sql id=”9j9h8h”
CREATE TABLE table1(x, y, z);
CREATE INDEX i1 ON table1(x);
CREATE INDEX i2 ON table1(y);




Now look at this query:



```sql id="d1km7x"
SELECT z FROM table1 WHERE x = 5 AND y = 6;

SQLite has two choices:

  • Use index i1 to find rows where x = 5, then filter y = 6
  • Use index i2 to find rows where y = 6, then filter x = 5

Both approaches are valid, but they may have very different costs.

How SQLite Chooses the Best Index

SQLite estimates how much work each option will require and chooses the one with the lowest cost.

The decision is based on heuristics such as:

  • How many rows are expected to match
  • How selective the index is
  • How much filtering is needed after lookup

If statistical data is available, SQLite makes better decisions.

This is where the sqlite_stat1 table comes into play.

It stores information about how many rows are typically associated with a given column value.

Using this data, SQLite can estimate which index will reduce the result set the most.

The index that is expected to return fewer rows is usually preferred.

Forcing SQLite to Ignore an Index

Sometimes, you may want to override SQLite’s choice.

SQLite provides a subtle mechanism for this using the unary + operator.

Example:

“`sql id=”u4cmnf”
SELECT z FROM table1 WHERE +x = 5 AND y = 6;




The `+` operator does nothing functionally, but it prevents SQLite from using the index on column `x`.

This forces the optimizer to consider other indexes, such as `i2` on column `y`.

This is a lightweight way to guide the optimizer without changing query semantics.

## Balancing WHERE and ORDER BY

Index selection is not only about filtering rows. 

SQLite also considers sorting requirements.

For example:



```sql id="z2t3r8"
SELECT * FROM table1 WHERE x = 5 ORDER BY y;

Now SQLite has a trade-off:

  • Use an index on x to filter efficiently
  • Use an index on y to avoid sorting

SQLite evaluates both options and chooses the one that results in the fastest overall execution.

This means sometimes SQLite may sacrifice a slightly less efficient filter to avoid an expensive sort operation.

When Sorting Cannot Use an Index

If no suitable index can satisfy the ORDER BY clause, SQLite must sort the results manually.

This is done using a temporary sorter, which behaves like a transient index.

The process looks like this:

open sorter
where-begin
    extract required columns
    build a record
    generate sort key
    insert into sorter
where-end
sort
for each sorted entry
    extract data
    return result
close sorter

This approach works, but it comes with a cost.

  • Additional memory usage
  • Extra processing for sorting
  • Potential performance overhead for large datasets

That is why SQLite always tries to use an index for ORDER BY whenever possible.

Why Index Selection Is So Important

At this stage, multiple layers of optimization come together:

  • WHERE clause determines candidate indexes
  • Join ordering decides when a table is accessed
  • Index selection decides how it is accessed
  • ORDER BY may influence which index is preferred

A single decision here can affect:

  • Number of rows scanned
  • Need for sorting
  • Overall execution time

Wrapping Up

Index selection is one of the most critical decisions in SQLite’s optimizer.

Even when multiple indexes are available, SQLite carefully evaluates which one minimizes total work.

It also balances filtering efficiency with sorting requirements to produce the fastest possible execution plan.

Understanding how SQLite makes this decision helps you design better indexes and write queries that align with the optimizer.

In the next part, we will explore GROUP BY and MIN/MAX optimizations, where SQLite applies additional strategies to aggregate and summarize data efficiently.

git-lrc
*AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*

Any feedback or contributors are welcome! It’s online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:

GitHub logo

HexmosTech
/
git-lrc

Free, Unlimited AI Code Reviews That Run on Commit

AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

See It In Action

See git-lrc catch serious security issues such as leaked credentials, expensive cloud
operations, and sensitive material in log statements

git-lrc-intro-60s.mp4

Why

  • 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won’t notice until production.
  • 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
  • 🔁 Build a…

I Built an AI That Recommends Therapy Pets — Here’s What I Learned (and the Code)

We’ve all heard about therapy dogs. But what if you’re allergic? Or live in a small apartment? Or just… really vibe with guinea pigs?

That was the problem I set out to solve. The result was an AI-powered pet therapy matching system. Here’s the full tech breakdown — and a few things that surprised me.

The Core Problem

Pet therapy is genuinely effective for anxiety, loneliness, and depression. Studies back it up. But “just get a dog” is terrible advice for:

  • Apartment dwellers
  • People with allergies
  • Those with limited mobility
  • Anyone who’s never owned a pet and has zero idea what they’re getting into

The matching problem is actually fascinating from an ML perspective: you’re trying to align human emotional needs, lifestyle constraints, and animal behavioral profiles — three completely different feature spaces.

The Architecture

User Input (survey)
        ↓
  Feature Extraction
        ↓
  Embedding Layer (sentence-transformers)
        ↓
  Similarity Scoring against Animal Profiles
        ↓
  Ranked Recommendations + Explanations
        ↓
  LLM-generated personalized summary

Simple. But the devil is in each layer.

Step 1: The Survey → Feature Vector

We don’t ask “what pet do you want?” (everyone says dog). We ask behavioral and lifestyle questions:

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')

def encode_user_profile(survey_responses: dict) -> np.ndarray:
    """
    Convert free-text + structured survey answers into a dense vector.
    """
    # Combine all responses into a natural-language description
    profile_text = f"""
    Activity level: {survey_responses['activity_level']}.
    Living situation: {survey_responses['living_situation']}.
    Primary emotional need: {survey_responses['emotional_need']}.
    Time available daily: {survey_responses['daily_time_hours']} hours.
    Experience with animals: {survey_responses['experience']}.
    Allergies or restrictions: {survey_responses.get('restrictions', 'none')}.
    """

    return model.encode(profile_text)

# Example
user = encode_user_profile({
    "activity_level": "low, mostly sedentary",
    "living_situation": "small apartment, no yard",
    "emotional_need": "companionship and stress relief",
    "daily_time_hours": 1.5,
    "experience": "never owned a pet",
    "restrictions": "mild cat allergy"
})

Why sentence-transformers over a rule-based system? Because humans describe themselves weirdly. “I work from home 80% of the time” and “I’m home most days” should map to the same feature space. Embeddings handle this naturally.

Step 2: The Animal Profile Database

This is where we encode animal behavioral data the same way:

animal_profiles = [
    {
        "id": "rabbit_holland_lop",
        "name": "Holland Lop Rabbit",
        "description": """
        Quiet, affectionate, and apartment-friendly. Doesn't require outdoor walks.
        Hypoallergenic option for mild allergy sufferers. Responds well to gentle 
        handling and routine. Ideal for calm, indoor environments. Moderate time 
        commitment — about 1-2 hours of interaction daily is ideal.
        """
    },
    {
        "id": "guinea_pig_pair",
        "name": "Guinea Pig (bonded pair)",
        "description": """
        Social, vocal, low-maintenance. Perfect for small spaces. No allergy concerns 
        for most people. Highly responsive to their owners — will 'wheek' when they 
        hear you. Great for emotional support without high physical demands.
        """
    },
    # ... more profiles
]

# Pre-compute embeddings for all animal profiles
animal_vectors = {
    p["id"]: model.encode(p["description"])
    for p in animal_profiles
}

Step 3: Cosine Similarity Scoring

from sklearn.metrics.pairwise import cosine_similarity

def rank_recommendations(user_vector: np.ndarray, animal_vectors: dict) -> list:
    scores = []

    for animal_id, animal_vec in animal_vectors.items():
        sim = cosine_similarity(
            user_vector.reshape(1, -1),
            animal_vec.reshape(1, -1)
        )[0][0]
        scores.append((animal_id, float(sim)))

    # Sort by similarity descending
    return sorted(scores, key=lambda x: x[1], reverse=True)

top_matches = rank_recommendations(user, animal_vectors)
# → [("guinea_pig_pair", 0.87), ("rabbit_holland_lop", 0.82), ...]

Step 4: LLM-Generated Explanations

The raw similarity score means nothing to a user. We pass the top matches to an LLM to generate a warm, personalized explanation:

import anthropic

client = anthropic.Anthropic()

def generate_explanation(user_survey: dict, top_match: dict) -> str:
    prompt = f"""
    A person with the following profile:
    {user_survey}

    Has been matched with: {top_match['name']}
    Match reason (technical): {top_match['description']}

    Write a warm, 2-3 sentence explanation of WHY this pet is a great match for them.
    Be specific to their situation. Avoid generic phrases.
    """

    message = client.messages.create(
        model="claude-3-5-haiku-20241022",
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}]
    )

    return message.content[0].text

This is the piece that makes users go “wow, it really gets me” — even though the underlying logic is just cosine similarity on text embeddings.

What Surprised Me

1. The “experience” feature matters more than I expected.

First-time pet owners consistently matched poorly with high-maintenance animals even when lifestyle fit was good. I had to add a soft penalty for mismatched experience levels.

2. Allergy handling is genuinely hard.

Allergy severity is non-linear. A “mild” cat allergy might be fine with a Siberian cat (lower Fel d 1 protein) but terrible with a Persian. I ended up adding a separate allergy-screening layer before the main matching.

3. Users lie on surveys (unconsciously).

People say they have 2 hours/day but actually mean 30 minutes. The solution? Add a calibration question: “How many hours do you spend on [comparable hobby] per week?” Cross-referencing revealed the actual available time.

Results After 3 Months

  • 78% of matched users reported being “very satisfied” with their recommendation at 30-day follow-up
  • Average session length: 4.2 minutes (users actually engage with the explanations)
  • Most surprising top match: Axolotls. Who knew they’d be such a good anxiety companion for people who “just want to watch something calming”?

Try It Live

If you want to see this in action — or if you’re genuinely curious what your ideal therapy animal is — the live matching system is at mypettherapist.com. Free to use, no signup required.

The codebase uses Python + FastAPI on the backend with React on the front. Happy to share more implementation details in the comments.

TL;DR

  • Use sentence-transformers to embed both user profiles and animal behavioral descriptions into the same vector space
  • Cosine similarity does the matching heavy lifting
  • LLM-generated explanations are what actually make users trust the result
  • Allergy handling and experience calibration are underrated complexity sources

What’s the weirdest AI matching problem you’ve tackled? Drop it in the comments — genuinely curious.

GDPR for Ecommerce: Customer Orders, Abandoned Carts, and Retargeting

Running an online shop means processing personal data at every stage of the customer journey — from the first page view to the final returns label. GDPR applies to all of it.

This guide covers the highest-risk areas for ecommerce businesses: order and delivery data, abandoned cart emails, retargeting pixels, email marketing consent, payment processors, customer deletion rights, product reviews, returns and fraud detection, age verification, international transfers, and data breach response.

Order and Delivery Data

Every completed order generates a rich personal data record: name, email, delivery address, payment reference, and order history. The lawful basis is contractual necessity (Article 6(1)(b)). Keep order records for VAT/tax compliance periods (typically 6-7 years), then delete. Every fulfilment warehouse, courier, and logistics provider must have a Data Processing Agreement (DPA) in place.

Abandoned Cart Emails: Consent vs Legitimate Interest

Abandoned cart emails are one of the most contested GDPR areas for ecommerce. Some argue a single timely reminder falls within legitimate interests — but under PECR, sending marketing emails to individuals requires prior consent unless the soft opt-in exemption applies. A guest checkout with no marketing opt-in does not satisfy the soft opt-in. The safest route: collect a separate marketing opt-in at checkout and limit abandoned cart sequences to subscribers.

Retargeting Pixels: Meta, Google, and Cookie Consent

Running Facebook Pixel, Google Ads tags, or TikTok Pixel requires prior, explicit, informed consent before the pixels fire. Your cookie consent banner must block all advertising pixels until the user actively accepts them, allow category-by-category acceptance, and record consent with a timestamp. Do not pre-tick advertising consent or use dark patterns.

Email Marketing and Double Opt-In

Consent must be freely given, specific, informed, and unambiguous. Double opt-in is strongly recommended — it provides clear evidence of consent and helps demonstrate compliance. Maintain a consent record for every subscriber including timestamp, form version, and consent wording.

Payment Processors and Shipping Partners

Stripe and PayPal typically act as independent data controllers for their own fraud and compliance purposes — disclose this in your privacy policy. Couriers and fulfilment providers are your data processors and require DPAs. If they transfer data outside the UK/EEA, your DPA must include Standard Contractual Clauses.

Customer Deletion Rights

GDPR’s right to erasure means customers can request account deletion. Delete all data not required for legal retention purposes, retain order records for the required period, remove them from marketing lists immediately, and confirm deletion in writing. Make the process easy — the ICO has taken a dim view of organisations that make erasure deliberately difficult.

Fraud Detection and Automated Decision-Making

If you use automated fraud scoring to decline orders without human review, you may be engaging in automated decision-making under Article 22. Where decisions have a significant effect on individuals, you must inform customers, give them the right to human review, and explain the logic in your privacy policy.

International Transfers

Shipping internationally means transferring customer data to countries outside the UK/EEA. Ensure your DPAs with international fulfilment partners include adequate safeguards — typically Standard Contractual Clauses.

Data Breach Response

Notify the ICO within 72 hours of becoming aware of a breach posing risk to individuals. Notify affected customers where there is high risk to their rights and freedoms. Document every breach and your response, even if you decide not to notify.

Scan your ecommerce store for compliance gaps at https://app.custodia-privacy.com/scan — free, no signup required, results in 60 seconds.

Turning Weekly GitHub Activity Into Blog Posts on Notion + DEV.to

DevNotion pipeline Architecture

This is a submission for the Notion MCP Challenge

What I Built

Every Monday standup, someone asks: “What did you work on last week?” And every Monday, I stare at my screen trying to remember. Did I merge that PR on Wednesday or Thursday? Was that refactor in the auth module or the pipeline? How many repos did I even touch?

I got tired of that blank moment. So I built DevNotion — a 3-agent pipeline powered by Mastra that harvests my entire week of GitHub activity, narrates it into a first-person blog post using Gemini, and publishes it to Notion (as a planner-style page with structured tables) and DEV.to (as a draft article). Every Sunday, automatically, via GitHub Actions.

No more Monday amnesia. The blog writes itself.

What it actually does

  1. Harvests my GitHub activity via GraphQL — commits, PRs, issues, code reviews, discussions, language stats, contribution streak
  2. Narrates the raw data into a casual, first-person blog post using Gemini (with a deterministic fallback if the LLM is unavailable)
  3. Publishes to two platforms simultaneously:

    • Notion — a planner-style page with stats tables, repo breakdowns, PR/issue/review tables, language breakdown, and the full blog post
    • DEV.to — a draft article ready for review

Key features

  • 3 specialized agents — each does one thing well (harvest, narrate, publish)
  • LLM only where it adds value — harvest and publish are deterministic, zero token overhead
  • 4 blog tone profiles — casual (default), professional, technical, storytelling
  • Planner-style Notion pages — not just a wall of text, but structured tables with stats, repos, PRs, issues, reviews, and languages
  • Notion MCP integration — full Notion API surface via Model Context Protocol
  • Notion Markdown Content API — write rich markdown directly to pages (the real game changer)
  • DEV.to draft publishing — articles created as drafts, ready to review and publish
  • GitHub Actions CI — weekly cron (Sundays 08:00 UTC) + manual dispatch
  • Blog log in README — CI auto-commits a metrics table after each run
  • Fallback chain — always produces a blog post, even if Gemini is down
  • Rate limiting everywhere — p-queue + p-retry for both Notion and DEV.to APIs

Architecture

DevNotion pipeline Architecture

Step Agent What it does
Harvest github-harvest-agent Fetches weekly GitHub data via GraphQL (deterministic)
Narrate narrator-agent Writes a first-person blog post from the data
Publish publisher-agent Creates Notion planner page + DEV.to draft via direct APIs

The pipeline only uses an LLM where it genuinely adds value — narration. Harvest and publish are pure function calls. No token overhead, no hallucination risk, faster execution.

Architecture Deep-Dive

Why 3 agents, not 1?

I could’ve built one mega-agent that does everything. But that’s a recipe for:

  • Burning tokens on deterministic work (fetching GitHub data doesn’t need an LLM)
  • Hallucinating URLs and stats (the publish step should never make things up)
  • Debugging nightmares (which part of the monolith failed?)

Instead, each agent is a specialist. The workflow chains them together:

export const weeklyDispatchWorkflow = createWorkflow({
  id: 'weekly-dispatch',
  inputSchema: z.object({ weekStart: z.string() }),
  outputSchema: PublishOutputSchema,
})
  .then(harvestStep)
  .then(narrateStep)
  .then(publishStep)
  .commit();

Three steps, chained with .then(), committed as a single workflow. Mastra handles the data handoff between steps automatically.

Harvest: deterministic data, zero LLM

The harvest step calls GitHub’s GraphQL API directly — no agent reasoning needed:

const harvestStep = createStep({
  id: 'harvest-github',
  inputSchema: z.object({ weekStart: z.string() }),
  outputSchema: WeeklyDataSchema,
  execute: async ({ inputData }) => {
    const data = await fetchWeeklyContributions(inputData.weekStart);
    return WeeklyDataSchema.parse(data);
  },
});

One GraphQL query pulls commits, PRs, issues, reviews, discussions, language stats, and contribution streak for the week. The response gets validated through a Zod schema. No LLM in the loop — this is pure data fetching.

Narrate: LLM with a fallback chain

This is where the LLM earns its keep. The narrator agent takes raw JSON and writes a blog post that sounds like I wrote it myself. The system prompt has a full personality profile — it writes in first person, knows my tech stack (Python, Rust, TypeScript), references my OSS work, and matches one of four tone profiles.

But LLMs can be flaky. So the narrate step has a fallback chain:

const narrateStep = createStep({
  id: 'narrate',
  execute: async ({ inputData, mastra }) => {
    const agent = mastra!.getAgent('narrator-agent');
    let blog;

    try {
      const result = await agent.generate(prompt);
      const parsed = parseFrontmatter(result.text);
      if (parsed.success) {
        blog = parsed.data.blog;
      } else {
        blog = buildFallbackNarration(inputData).blog;
      }
    } catch (err) {
      blog = buildFallbackNarration(inputData).blog;
    }

    return { blog, weeklyData: inputData };
  },
});
  1. Gemini generates a markdown blog with YAML frontmatter
  2. If parsing fails → deterministic fallback builds a basic post from raw data

A blog post is always produced, even if Gemini is completely down.

Publish: Notion planner + DEV.to draft

The publish step is where things get interesting. It doesn’t just dump text into Notion — it builds a planner-style page with structured tables:

const publishStep = createStep({
  id: 'publish',
  execute: async ({ inputData }) => {
    const { blog, weeklyData } = inputData;

    // 1. Create Notion page
    const createResult = await createNotionPage(title);

    // 2. Create DEV.to draft (so the link goes into the Notion planner)
    const devtoResult = await createDevtoArticle({
      title: blog.headline,
      body_markdown: buildDevtoMarkdown(blog),
      tags: blog.tags,
      published: false,
    });

    // 3. Write planner markdown to Notion (includes DEV.to link)
    const plannerMd = buildPlannerMarkdown(weeklyData, blog, links);
    await writeNotionMarkdown(notionPageId, plannerMd);
  },
});

The order matters: DEV.to draft gets created before writing the Notion page content, so the Notion planner can include a link to the DEV.to draft. Cross-platform linking, done right.

Each Notion page includes:

  • Published Links table — Notion page URL + DEV.to draft edit link
  • Week at a Glance — commits, PRs, issues, reviews, lines added/removed, streak
  • Active Repositories — repo name, commits, language, line changes
  • Pull Requests / Issues / Reviews / Discussions — structured tables
  • Languages — top languages by commit count
  • Full blog post — the narrated content below a separator

// Detect dark theme
var iframe = document.getElementById(‘tweet-2037594562424914027-412’);
if (document.body.className.includes(‘dark-theme’)) {
iframe.src = “https://platform.twitter.com/embed/Tweet.html?id=2037594562424914027&theme=dark”
}


DevNotion Project

Star DevNotion on GitHub

How I Used Notion MCP

This is the part I’m most excited about. DevNotion uses the Notion MCP Server in two complementary ways:

1. Notion MCP Server via @mastra/mcp

The publisher agent integrates with the official @notionhq/notion-mcp-server through Mastra’s MCP client. This gives the agent access to the full Notion API surface via Model Context Protocol:

import { MCPClient } from '@mastra/mcp';

export const notionMcp = new MCPClient({
  servers: {
    notion: {
      command: 'npx',
      args: ['-y', '@notionhq/notion-mcp-server'],
      env: {
        OPENAPI_MCP_HEADERS: JSON.stringify({
          Authorization: `Bearer ${env.NOTION_TOKEN}`,
          'Notion-Version': '2022-06-28',
        }),
      },
    },
  },
  timeout: 30000,
});

The MCP tools are loaded lazily with a graceful fallback — if the MCP server fails to start, the direct tools still work independently:

export async function getNotionMcpTools(): Promise<Record<string, any>> {
  try {
    return await notionMcp.listTools();
  } catch (err) {
    console.warn('MCP: Notion MCP server unavailable, using direct tools only');
    return {};
  }
}

2. Direct tools + MCP tools merged

The publisher agent merges both tool sets — MCP tools for the full Notion API surface, and direct tools for capabilities MCP doesn’t cover:

// Direct tools (Markdown Content API + DEV.to — not available via MCP)
const directTools = {
  createNotionPage: createNotionPageTool,
  writeMarkdown: writeMarkdownTool,
  searchNotion: searchNotionTool,
  updateNotionPage: updateNotionPageTool,
};

// Merge: Notion MCP tools + direct tools
const mcpTools = await getNotionMcpTools();
const tools = { ...mcpTools, ...directTools };

This dual approach means the publisher agent gets the best of both worlds — MCP’s broad API surface for interactive use in the Mastra playground, plus direct tools for the automated workflow.

3. The Markdown Content API (the game changer)

This is the Notion feature that made the planner-style pages possible. Instead of constructing Notion blocks one by one (which is painful and rate-limit-heavy), I write the entire page as markdown in one API call:

const response = await fetch(
  `https://api.notion.com/v1/pages/${pageId}/markdown`,
  {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${env.NOTION_TOKEN}`,
      'Content-Type': 'application/json',
      'Notion-Version': '2026-03-11',
    },
    body: JSON.stringify({
      type: 'replace_content',
      replace_content: { new_str: markdown },
    }),
  },
);

One PATCH request replaces the entire page content with rich markdown — including tables, headings, blockquotes, links, code blocks, everything. This is what powers the planner-style layout with structured stats tables + the full blog post, all in a single API call.

4. Rate limiting

Notion’s API allows roughly 3 requests per second. Every Notion call (MCP and direct) goes through a shared rate limiter:

const queue = new PQueue({ concurrency: 1, interval: 334, intervalCap: 1 });

async function rateLimited<T>(fn: () => Promise<T>): Promise<T> {
  return queue.add(() => pRetry(fn, { retries: 3 })) as Promise<T>;
}

p-queue throttles concurrency, p-retry handles transient failures. I learned this the hard way — without rate limiting, the Notion API will 429 you into oblivion when you’re creating a page, writing markdown, and updating the icon in quick succession.

Lessons Learned

Rate limits are the real boss

Notion (3 req/s), DEV.to (30 req/30s), GitHub GraphQL (5000 points/hr) — every API has its own throttle. I ended up with p-queue + p-retry wrappers around everything. The rate limiter code is almost identical across all three services, and honestly, it should probably be a shared utility. But three similar lines of code is better than a premature abstraction.

Structured output is slower than you’d think

I originally used Gemini’s native JSON schema for structured output (agent.generate(prompt, { structuredOutput: { schema } })). It worked, but added 20-40 seconds per call. Switching to plain text generation with YAML frontmatter parsing was 3-4x faster and just as reliable. The deterministic fallback catches the rare parsing failure.

Gemini model musical chairs

I’ve been through three Gemini models on this project: gemini-2.5-flash-preview-04-17 (retired), gemini-2.5-flash (stable but slow for structured output), and now gemini-3-flash-preview (current). The lesson: always make the model configurable via env vars. Hardcoding model IDs is a recipe for broken deploys.

The Zod conflict that broke everything

Mastra and my code both depend on Zod, but different versions. Having two Zod instances means z.string() from one isn’t recognized by the other — schema validation just silently fails. The fix: a single line in package.json:

{
  "pnpm": {
    "overrides": {
      "zod": "$zod"
    }
  }
}

Forces pnpm to deduplicate to one Zod version. Took me way too long to figure that out.

Direct API calls beat agent reasoning for deterministic work

The harvest and publish steps started as full agent calls. But an LLM doesn’t add anything when the task is “call this GraphQL endpoint and return the result.” Switching to direct function calls made the pipeline faster, cheaper, and more predictable. Only use an LLM where you need creativity or reasoning — everywhere else, just write a function.

Built with Mastra, Gemini, Notion API, and a lot of coffee. If you’ve ever forgotten what you worked on last week, give DevNotion a try.

CreatorConfig: PCPartPicker for streaming & podcast gear

CreatorConfig helps you build a streaming, podcasting, or content creation setup that actually works before you buy. Get compatibility checking, setup warnings, and complete gear lists with one click.

The Stack

  • Frontend: React + TypeScript + Vite
  • Styling/UI: Tailwind CSS + custom component patterns
  • Backend: Node.js (ESM) + Express-style API routes
  • Database: PostgreSQL

The Product

CreatorConfig is a creator gear setup platform where users can build, save, and share complete streaming/podcasting/content-creation setups.

Core product pillars:

  • Builder-first workflow: pick parts by category (mic, camera, interface, mixer, etc.) in a structured flow.
  • Setup publishing system: keep setups private, publish publicly, or feature curated completed setups.
  • Setup detail pages: each setup has its own URL, story content, product breakdown, and buy links.
  • Profile ecosystem: users can manage setups, saved items, and activity from account/profile pages.
  • Discovery layer: browse parts, completed setups, and community builds with cleaner SEO-friendly routes.

Goal: make choosing creator gear less guesswork and more systemized.

NPM Archaeology: 5 Years in the Ground, Still Breathing

Let me set the scene: It’s a Saturday. Nothing particularly dramatic is happening. I’m just sitting there, scrolling through my old GitHub repos kinda digital archaeologist, and I find it
— react-calendar-datetime.
My old and abandoned calendar package.
Version 1.3.1 Last update: five years ago. ~100 downloads a month. A couple of themes. A moment.js dependency that probably weighs more than the entire package should.

I don’t know what came over me. Maybe it was lazy weekend. Maybe it was nostalgia. Maybe I just wanted to feel something. Either way, I opened it up and thought: let’s see how bad this is.

State of Things

Honestly? It wasn’t that bad. The core worked. The calendar rendered dates correctly and didn’t catch fire when you clicked on it. By the standards of “abandoned weekend projects”, this was actually in pretty good shape.

But I cannot look at working code and leave it alone. I wanted to try to optimize it, pour in everything I’d learned over the past 5 years — and if that didn’t work out, at least break something 😀

So what started as “let me just poke around” turned into a two-week sprint that took the package from v1.3.1 all the way to v3. I have no one to blame but myself.

The First Boss: TypeScript

The first real challenge was migrating to TypeScript. I’d been putting this off for years — not because I don’t like TypeScript (I really do), but because migrating an existing JS project to TS is exactly as fun as it sounds. You think you’re just adding types. You are not just adding types. You start finding every lazy shortcut you ever took with your data, all at once, while the compiler just stares at you and judges.

It took a while. But once it was done, everything felt more solid. Like the project had grown a spine.

The Dependency Problem (Or: My Obsession With Bundle Size)

Here’s something I didn’t expect to become a personality trait: I got absolutely fixated on bundle size.
It started innocently. The original package had moment.js. Classic. I migrated to dayjs, felt very smart about it, moved on. Then I started actually measuring the bundle.

Then I couldn’t stop.

Then I started resenting every single byte that wasn’t mine.
So I removed dayjs too 😀
Wrote my own date utilities. They’re not fancy — no one’s going to write a blog post about my addMonths function — but they work, they’re pretty small, and they belong to me.

Final score: zero external dependencies. Bundle stayed around: ~5-7kb gzipped. I check this number more often than I’d like to admit.

The CSS Saga

I moved all styles from SCSS to goober, a CSS-in-JS library. Felt modern. Felt good. 1kb of injected code. Shipped it.

But I couldn’t stop thinking about zero dependency library.

And what we have now: CSS Modules. No more goober. I slept better after this decision and I’m not joking. At least for next few weeks 😀

The Localization Rabbit Hole

This is my favorite part of the whole story, because it went from “annoying problem” to “surprisingly elegant solution” in a way that felt like cheating.
Originally, locales were only supported for presets — “Today”, “Yesterday”, etc. and just for few languages.
Then I added dayjs locale support for month names, weeks…
This was… fine, but the list of supported locales was limited, and every new locale added weight to the bundle. (Remember my obsession?)

The more languages I wanted to support, the fatter the package got.

It was a trap.

Then I discovered Intl — the native browser internationalization API. And I just… used that instead.

400+ locales. Month names, weekday names, relative time formatting for presets — all of it, natively, with zero extra bytes. No dictionaries. No JSON files. No dependencies.

I just sat back and stared at the ceiling for a moment. Sometimes the platform just gives you what you need and you’ve been ignoring it.

The Theme System (My Proudest Weird Thing)

At some point I started making themes. Then I made more themes. Then I couldn’t stop making themes. The package now has 18 of them — dark, light, brutalist, pastel, neon — and I love each one like a slightly chaotic parent.

But more importantly: trying to manage all these themes efficiently (bundle size!) pushed me to design a proper theming system based on CSS variables.
Seven variables per theme, one lookup table, predictable output. That system ended up being clean enough that I extracted it into a separate npm package

We all need more NPM packages 😀

That’s just what happens when you get bored on a weekend.

The Easy Win I Didn’t Expect

Migrating from rollup to tsup, and later to tsdown — I expected this to be a nightmare. Build tooling migrations have a reputation, and it’s not a good one.
It was fine. It was suspiciously fine. The authors of these tools did a great job and I got to just… configure a few things and move on. After weeks of one thing leading to another, this was a nice surprise.

Where Things Stand

Two weeks of obsessive weekend coding later:

v3.1.2 is out

  • Fully modular: toggle time picker, month grid, year selector, presets independently
  • 18 themes with a CSS variable system, gradients, shades.
  • 400+ locales via Intl
  • Week numbers, weekend highlighting, swipe gestures, jelly mode (don’t ask, just try it)
  • ~5.9kb gzipped
  • Accessibility work in progress (there’s always more to do here, I know)

Downloads went from 100/month to… more than 100/month. I’m not going to oversell it. But people are using it, filing issues, and occasionally saying nice things, which is more than I had before.

The Honest Conclusion

Open source has always made me nervous — I’ve tried before and it never really clicked. This time feels different, maybe because I’m not trying to make something important. I’m just trying to make something good.
If you have an old project collecting dust somewhere: maybe don’t let it sit for five years. Or do.

Get bored on a Saturday.

See what happens.
The package is react-calendar-datetime if you want to take a look. PRs welcome. Issues welcome. Gentle feedback about my themes collection absolutely welcome.

How to send sensor data to a gateway using PainlessMesh in ESP8266?

Hello, I’m Ganesh. I’m building git-lrc, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product.

In this article, I will be demonstrating how to use PainlessMesh with a NodeMCUv2 (ESP8266) board and send data from one ESP8266 board to another ESP8266 board.

What Problem we are solving?

Previously we explored how to use MQTT with ESP8266.

Now let’s integrate painlessmesh with MQTT.

Let’s assume we have 2 ESP8266 boards.
One is gateway and another is node.

We want to send data from node to gateway using painlessmesh. Next we want to send data from gateway to cloud using MQTT.

For example we will use DHT11 sensor and mq series gas sensor with node to send data to gateway.

This operation should be done with very minimal data transfer.

If one node has 2 or more sensors it should be compressed and sent to gateway.

What We will be achieving?

Painlessmesh is very heavy library. It does lot of operations to keep network alive. With this if we send data without compressing it. It will consume lot of power and data. Casuing network to be unstable.

Main Goal is

  1. Receive data from multiple sensors
  2. Compress the data
  3. Send the data to gateway
  4. Publish the data from received at gateway to cloud using MQTT
  5. Scallable MQTT topics publishing

I will be combining all the above mentioned in single project.

In this article we will be covering first 2 points.

  1. Receiving Data from Multiple Sensors
  2. Compressing the data
  3. Send the data to gateway

Receiving Data from Multiple Sensors and Compressing it

For this example we will use DHT11 sensor and MQ2 gas sensor.

  1. Setup connection with painlessmesh
    We will use these credentials for painlessmesh.
#define MESH_PREFIX "secreat_name"
#define MESH_PASSWORD "SecreateKey"
#define MESH_PORT 5555

With this funtion we create scheduler and painlessmesh object.

#include <painlessMesh.h>

  mesh.init(MESH_PREFIX, MESH_PASSWORD, &scheduler, MESH_PORT);
  mesh.onReceive(&onReceive);
  mesh.onNewConnection(&onNewConnection);
  1. Setup connection with DHT11 sensor

We use DHT library sensor to measure temperature and humidity.

We use D2 pin to connect DHT11 sensor.

#include <DHT.h>
#define DHT_PIN D2
#define DHT_TYPE DHT11

DHT dht(DHT_PIN, DHT_TYPE);

Read the temperature and humidity from the sensor.

float t = dht.readTemperature();
float h = dht.readHumidity();

As Dht sensor should have 1 second delay between readings. We will use scheduler to read the temperature and humidity from the sensor.

Task readDHTTask(10000, TASK_FOREVER, []() {
    float t = dht.readTemperature();
    float h = dht.readHumidity();
    if (isnan(t) || isnan(h)) {
        Serial.println("Failed to read from DHT sensor!");
        return;
    }
    Serial.printf("Temperature: %.2f C, Humidity: %.2f %%", t, h);
});
  1. Setup connection with MQ2 gas sensor

We use MQ2 gas sensor to measure the gas concentration.

We use A0 pin to connect MQ2 gas sensor.

#define MQ2_PIN A0

Read the gas concentration from the sensor.

float gas = analogRead(MQ2_PIN);

As MQ2 sensor should have 1 second delay between readings. We will use scheduler to read the gas concentration from the sensor.

Task readMQ2Task(10000, TASK_FOREVER, []() {
    float gas = analogRead(MQ2_PIN);
    if (isnan(gas)) {
        Serial.println("Failed to read from MQ2 sensor!");
        return;
    }
    Serial.printf("Gas: %.2f ppm", gas);
});
  1. Compress the data with bit packing

We use bit packing to compress the data.

As sensor will be in specified range we can use bit packing to compress the data.

String packToHex(float temp, float hum, float gas) {
  uint16_t packedTemp = (uint16_t)(temp * 10.0f) & 0x1FF;         // 9 bits
  uint16_t packedHum = (uint16_t)((hum - 20.0f) * 10.0f) & 0x3FF; // 10 bits
  uint16_t packedGas = (uint16_t)(gas / 10.0f) & 0x3FF;           // 10 bits

  uint32_t packed = 0;
  packed |= (uint32_t)packedTemp << 20; // bits 28-20
  packed |= (uint32_t)packedHum << 10;  // bits 19-10
  packed |= (uint32_t)packedGas;        // bits  9-0

  char buf[9];
  snprintf(buf, sizeof(buf), "%08X", packed);
  return String(buf);
}

Finaly commbining all these into one code.

/**
 * sensor_node_1.cpp — PainlessMesh Sensor Node (DHT11 + MQ6)
 *
 * Boot flow:
 *   1. Joins the mesh network
 *   2. On first connection to gateway → broadcasts /config (packing schema)
 *      The gateway forwards /config to MQTT → backend caches the schema
 *   3. After 3 s delay → starts sending /data every 5 s
 *      (gives backend time to process /config before first hex payload arrives)
 *
 * /alert is sent immediately any time gas > GAS_THRESHOLD.
 *
 * Bit packing layout (29 bits used, 3 MSBs unused):
 *   bits 28-20 (9 bits) : temp  = temp_celsius * 10        (0–511 → 0–51.1°C)
 *   bits 19-10 (10 bits): hum   = (humidity - 20) * 10     (0–1023 → 20–122.3%)
 *   bits  9-0  (10 bits): gas   = gas_ppm / 10             (0–1023 → 0–10230
 * ppm)
 */

#include <Arduino.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <painlessMesh.h>

// ─── Mesh credentials (must match gateway) ───────────────────────────────────
#define MESH_PREFIX "secreat_name"
#define MESH_PASSWORD "SecreateKey"
#define MESH_PORT 5555

// ─── Sensor pins ─────────────────────────────────────────────────────────────
#define DHT_PIN D2
#define DHT_TYPE DHT11
#define MQ6_PIN A0 // analog pin

// ─── Alert threshold ─────────────────────────────────────────────────────────
#define GAS_THRESHOLD 5000 // ppm — triggers /alert

// ─── Timing ──────────────────────────────────────────────────────────────────
#define DATA_INTERVAL_MS 5000

// ─── Globals ─────────────────────────────────────────────────────────────────
Scheduler scheduler;
painlessMesh mesh;
DHT dht(DHT_PIN, DHT_TYPE);
bool configSent = false; // send /config once per gateway connection

// ─── Bit packing ─────────────────────────────────────────────────────────────

/**
 * Pack temperature, humidity and gas into an 8-char hex string.
 * The encoding matches what the backend's unpackHex() expects.
 *
 *   temp  → 9 bits, mult 10, min 0
 *   hum   → 10 bits, mult 10, min 20
 *   gas   → 10 bits, div 10, min 0
 */
String packToHex(float temp, float hum, float gas) {
  uint16_t packedTemp = (uint16_t)(temp * 10.0f) & 0x1FF;         // 9 bits
  uint16_t packedHum = (uint16_t)((hum - 20.0f) * 10.0f) & 0x3FF; // 10 bits
  uint16_t packedGas = (uint16_t)(gas / 10.0f) & 0x3FF;           // 10 bits

  uint32_t packed = 0;
  packed |= (uint32_t)packedTemp << 20; // bits 28-20
  packed |= (uint32_t)packedHum << 10;  // bits 19-10
  packed |= (uint32_t)packedGas;        // bits  9-0

  char buf[9];
  snprintf(buf, sizeof(buf), "%08X", packed);
  return String(buf);
}

// ─── Message builders
// ─────────────────────────────────────────────────────────

/**
 * Build the /config JSON that tells the backend how to decode our hex payloads.
 * This matches the NodeConfig struct on the backend.
 */
String buildConfigMsg() {
  JsonDocument doc;
  doc["type"] = "config";
  doc["hw"] = "NodeMCUv2";

  JsonObject packing = doc["packing"].to<JsonObject>();
  packing["bits"] = 32;
  JsonArray layout = packing["layout"].to<JsonArray>();
  layout.add("temp");
  layout.add("hum");
  layout.add("gas");

  JsonObject sensors = doc["sensors"].to<JsonObject>();
  JsonObject t = sensors["temp"].to<JsonObject>();
  t["model"] = "DHT11";
  t["metric"] = "temperature";
  t["min"] = 0;
  t["max"] = 50;
  t["mult"] = 10;
  t["bits"] = 9;

  JsonObject h = sensors["hum"].to<JsonObject>();
  h["model"] = "DHT11";
  h["metric"] = "humidity";
  h["min"] = 20;
  h["max"] = 90;
  h["mult"] = 10;
  h["bits"] = 10;

  JsonObject g = sensors["gas"].to<JsonObject>();
  g["model"] = "MQ6";
  g["metric"] = "gas_mq6";
  g["min"] = 0;
  g["max"] = 10000;
  g["div"] = 10;
  g["bits"] = 10;

  String out;
  serializeJson(doc, out);
  return out;
}

String buildDataMsg(const String &hexStr) {
  JsonDocument doc;
  doc["type"] = "data";
  doc["d"] = hexStr;
  String out;
  serializeJson(doc, out);
  return out;
}

String buildAlertMsg(const String &hexStr, const char *cause) {
  JsonDocument doc;
  doc["type"] = "alert";
  doc["cause"] = cause;
  doc["d"] = hexStr;
  String out;
  serializeJson(doc, out);
  return out;
}

// ─── Periodic data task
// ────────────────────────────────────────────────────────

Task dataTask(DATA_INTERVAL_MS, TASK_FOREVER, []() {
  float temp = dht.readTemperature();
  float hum = dht.readHumidity();

  // MQ6: raw analog 0-1023 → scale to ppm (rough calibration, adjust to yours)
  int raw = analogRead(MQ6_PIN);
  float gas = raw * (10000.0f / 1023.0f); // map 0-1023 → 0-10000 ppm

  if (isnan(temp) || isnan(hum)) {
    Serial.println("[sensor] DHT read failed");
    return;
  }

  Serial.printf("[sensor] temp=%.1f°C  hum=%.1f%%  gas=%.0f ppmn", temp, hum,
                gas);

  String hexStr = packToHex(temp, hum, gas);

  // Always send /data
  mesh.sendBroadcast(buildDataMsg(hexStr));

  // Send /alert if threshold breached
  if (gas > GAS_THRESHOLD) {
    Serial.println("[sensor] ⚠ Gas threshold breached — sending alert");
    mesh.sendBroadcast(buildAlertMsg(hexStr, "gas_mq6"));
  }
});

// One-shot task: fires 3 s after /config is sent, then enables dataTask.
// The 3 s gives the gateway time to publish /config to MQTT and the backend
// time to cache the packing schema before the first hex payload arrives.
Task startupTask(3000, 1, []() {
  Serial.println("[sensor] Config settle time elapsed — starting data stream");
  scheduler.addTask(dataTask);
  dataTask.enable();
});

// ─── Mesh callbacks
// ────────────────────────────────────────────────────────────
void onReceive(uint32_t from, String &msg) {
  Serial.printf("[mesh] From %u: %sn", from, msg.c_str());
  // Future: handle /cmd messages from gateway
}

void onNewConnection(size_t nodeId) {
  Serial.printf("[mesh] +Connected to %un", (uint32_t)nodeId);

  if (!configSent) {
    // First connection — push /config so backend can cache packing schema
    mesh.sendBroadcast(buildConfigMsg());
    Serial.println("[mesh] Sent /config — waiting 3 s before data stream");
    configSent = true;

    // Start the 3-second countdown before data begins
    scheduler.addTask(startupTask);
    startupTask.enable();
  }
}

// ─── Setup / Loop
// ─────────────────────────────────────────────────────────────
void setup() {
  Serial.begin(115200);
  delay(100);

  dht.begin();

  mesh.setDebugMsgTypes(ERROR | CONNECTION);
  mesh.setContainsRoot(
      true); // tells node the root (gateway) is in this network
  mesh.init(MESH_PREFIX, MESH_PASSWORD, &scheduler, MESH_PORT);
  mesh.onReceive(&onReceive);
  mesh.onNewConnection(&onNewConnection);

  Serial.printf("[mesh] Node ID: %u — waiting for gateway connection...n",
                mesh.getNodeId());
}

void loop() { mesh.update(); }

By above code we can send data from multiple sensors to gateway using painlessmesh.

Conclution

In this article we have seen how to use painlessmesh with a NodeMCUv2 (ESP8266) board and send data from one ESP8266 board to another ESP8266 board.

In next article we will integrate scallable gateway.

git-lrc

Any feedback or contributors are welcome! It’s online, source-available, and ready for anyone to use.
⭐ Star it on GitHub: https://github.com/HexmosTech/git-lrc

The OpenClaw Problem, or: How I Stopped Wrapping Docker in VMs and Built a One-Command Setup Instead

GitHub logo

A continuation of 128GB of RAM, Zero Internet, and a Year of Building AI Infrastructure Nobody Asked For

When I shipped headless agent mode for cloister, I thought I’d solved the hard problem. VM isolation, consent policies, credential forwarding — all the security layers you’d want between an autonomous AI agent and your actual life. The next step was obvious: run OpenClaw inside it.

OpenClaw is an open-source AI agent framework — a persistent assistant that connects to Telegram, WhatsApp, iMessage, and runs tasks on your behalf. It has a gateway (the brain), nodes (execution endpoints), and a sandbox for running arbitrary code. It’s powerful. It’s also the kind of software whose community has been refreshingly honest about: don’t run it on bare metal.

So I didn’t. I ran it inside a cloister VM. Inside Docker. Inside a Linux VM. On macOS.

If that sentence made you wince, you’re ahead of where I was.

Three Security Bypasses and a Caddy Proxy

The first sign something was architecturally wrong came when I tried to access OpenClaw’s Control UI through an SSH tunnel. Three things broke, each requiring a security bypass:

  1. CSP Header. OpenClaw’s CSP hashes didn’t match its actual inline scripts. The UI rendered as a blank page. Fix: a Caddy reverse proxy to strip and replace the header. Not ideal.

  2. Device Auth. Docker NAT meant the gateway saw 172.18.x.x instead of 127.0.0.1, triggering device pairing over HTTPS — which SSH tunnels don’t provide. Fix: dangerouslyDisableDeviceAuth: true. The flag’s name judged me every time I opened the config.

  3. Gateway Bind. Loopback binding + Docker port publishing don’t mix. Fix: gateway.bind: lan, which is exactly what the security docs tell you not to do.

I rationalized all three: “The VM has three isolation layers. Even with bypasses, the blast radius is contained.” Technically true. Also a Rube Goldberg machine where each layer existed to compensate for the layer below it.

Then I found OpenClaw’s Lume documentation.

The “Wait, They Already Solved This” Moment

OpenClaw has a docs page titled “macOS VM.” I’d skimmed past it. When I actually read it:

Lume runs macOS VMs on Apple’s Virtualization.framework — same hypervisor tech cloister uses through Colima for Linux, but with macOS guests. The key differences that made me rethink everything:

  • No Docker. OpenClaw installs natively. It has its own sandbox — the one I was redundantly wrapping in another Docker layer.
  • No CSP bypass. Gateway runs on actual localhost. No NAT, no proxy.
  • No auth bypass. Connections come from real localhost. Device pairing just works.
  • No bind hack. Gateway stays on loopback. Everything works as documented.
  • iMessage access. Lume VMs are real macOS — they can run BlueBubbles for iMessage bridging. Linux VMs can’t. Ever.

Five problems solved by removing a layer of abstraction. I chose the option that meant admitting the layer I’d built was wrong: abstract the VM backend so cloister supports both. Colima for Linux workloads, Lume for macOS. The user says --openclaw, cloister picks Lume. The user creates a regular profile, cloister picks Colima. Nobody needs to know which hypervisor is underneath.

The Installation Gauntlet

If you’ve tried setting up a Lume macOS VM yourself, you’ve probably already rage-quit once. Here’s what actually happens:

The IPSW download. Lume creates a macOS VM from Apple’s ~18GB restore image. It downloads to a temp directory. If the setup fails — and it will — the download is gone. Next attempt? Another 35 minutes on a decent connection. I built IPSW caching into cloister so it downloads once, keeps it at ~/.cloister/cache/ipsw/, and reuses it across every attempt and every profile. The kind of feature nobody thanks you for until they don’t have it.

The version mismatch. Apple’s Virtualization.framework refuses to install a macOS guest newer than the host. It downloads the 18GB IPSW, starts the install, and rejects it after 25 minutes. I added a preflight check that parses the IPSW filename version, compares against sw_vers, and fails in five seconds instead of twenty-five minutes.

The Setup Assistant. Lume automates the macOS Setup Assistant by clicking through 167 UI elements via VNC. On macOS 26.4, Apple renamed a button. Step 66 of 167 fails. Every time. This is an upstream Lume issue — if you’re doing it yourself, you’ll need to create the VM without --unattended and click through setup manually via VNC. Five minutes by hand. Not ideal, but you only do it once per base image.

The base image. cloister solves this with a shared base image that every OpenClaw profile clones from. First profile: 15-20 minutes. Every subsequent one: about 2 minutes. Plus snapshots — factory (after provisioning) and user (your fully configured state) — so cloister reset gets you back to working in under two minutes.

If you’re setting this up without cloister, budget an afternoon and a decent internet connection. Or save yourself the trouble.

The Real Problem: Nobody Knows Where to Start

The Lume setup gets you a blank macOS VM. Congratulations. Now you need to:

  1. Configure passwordless sudo
  2. Install Xcode Command Line Tools, Homebrew, Node.js, Playwright, OpenClaw
  3. Run the OpenClaw daemon onboard with the right flags
  4. Set up a Telegram bot via BotFather
  5. Figure out your Telegram user ID (hint: message @userinfobot)
  6. Configure Ollama as a provider — but first figure out the VM’s bridge gateway IP
  7. Set up Google OAuth for Gmail, Calendar, Drive — but the OAuth callback goes to localhost, and you’re inside a VM, so you need an SSH tunnel
  8. Deal with the macOS keychain password that drifted from the user password during headless setup
  9. Register the node host with OpenClaw’s device pairing system
  10. Approve the device — but not with --all because that flag doesn’t exist, it’s --latest

Each of these has at least one gotcha that’ll cost you 30 minutes of Googling. Some have gotchas that’ll cost you a day.

I know because I hit every single one of them.

cloister setup openclaw

So I built a wizard.

cloister create --openclaw my-oc    # Create the Lume VM (clones from base image)
cloister setup openclaw my-oc       # One command. Handles everything.

The wizard runs five sections in order. Each one writes config and credentials immediately — never batches at the end. If your WiFi drops halfway through, re-running the wizard picks up from the last completed step.

Credentials. Detects 1Password CLI. If found, stores everything there with Touch ID. If not, asks if you want to install it or use local encrypted storage. Generates a new keychain password (because the headless setup almost certainly left it in a state where the old one doesn’t work), stores it before applying it to the VM (credential sync invariant — never change a password you haven’t recorded), and stores the VM user credentials.

Channels. Walks you through BotFather for Telegram — or accepts an existing bot token. Collects your user ID with instructions to message @userinfobot. Writes the config with dmPolicy: allowlist and allowFrom locked to your Telegram ID. I initially used allowedUserIds because that’s what seemed logical. OpenClaw rejected it. The actual field is allowFrom. The kind of thing where you stare at a “Config invalid” error for twenty minutes before realizing the docs and the schema disagree.

WhatsApp gets configured as an action-only channel — OpenClaw can send you notifications, but nobody can issue commands through it. Safe defaults: allowCommands: false, allowFrom locked to your number, escalationPolicy: deny. Because the last thing you want is someone in a WhatsApp group accidentally telling your AI agent to rm -rf /.

Providers. Auto-detects Ollama on the VM’s bridge gateway IP. Lists available models. Offers to pull qwen3:32b if nothing’s loaded. Then asks the real question: local inference (free, fan sounds like a jet engine) or Anthropic Claude (costs money, quiet). Both can be registered simultaneously — you just pick a default. The wizard handles the API key collection and 1Password storage.

Google OAuth. This one was fun. gog (the Google CLI) needs to run inside the VM, but the OAuth callback redirects to localhost — which is the VM’s localhost, not your Mac’s. The wizard picks a random high port (49152–60999, collision-checked), sets up an SSH tunnel, runs gog auth add with --listen-addr 0.0.0.0:<port>, and the callback routes through the tunnel back to gog. You just paste the URL in your browser and click Allow.

Except gog’s file-backed keyring needs a password to encrypt tokens, and there’s no TTY available for the prompt. The wizard sets GOG_KEYRING_PASSWORD to the keychain password it already stored. Another 45 minutes of debugging, now a single environment variable in the setup flow.

Device Pairing. Registers the node host, writes trusted proxy config, approves pending devices, and runs openclaw gateway probe to verify the whole thing is actually healthy — WebSocket connectivity, RPC health, auth warnings. Not just “is the process running” but “can we actually talk to it.”

The whole flow takes about 3 minutes on a running VM. Non-interactive mode works too:

cloister setup openclaw my-oc 
  --telegram-token="BOT_TOKEN" 
  --telegram-user-id="USER_ID" 
  --default-provider=ollama 
  --ollama-model=qwen3:32b 
  --google-client-secret=~/client_secret.json 
  --google-email="[email protected]"

And for AI agents that want to discover what’s configurable:

cloister setup openclaw my-oc --list-options --json

The Config Serialization Bug That Almost Ate Everything

While building the wizard, I discovered that older versions of cloister (the one installed via Homebrew) would silently drop the backend: lume field when saving config. Go’s yaml.v3 library ignores struct fields it doesn’t recognize during unmarshal, so the old binary would load the config, not understand backend, and write it back without it. Next time the new binary loaded the config, it would default to Colima and everything would break.

The fix was three things: remove omitempty from the Backend field (always serialize it), add a config version number (so we can detect and migrate), and rotate config.yaml to config.yaml.prev before every save. If anything goes wrong, your previous config is one file rename away. Simple, boring, correct.

The Agent Command That Had to Die

The original cloister had a whole agent subcommand tree — agent start, agent stop, agent status, agent logs, agent forward. This made sense when OpenClaw ran inside Docker inside a Linux VM. The VM was one thing, the Docker container was another, and you needed separate controls for each.

With Lume, the VM is the agent. OpenClaw runs as a launchd service, not a Docker container. There’s no separate lifecycle to manage. So I replaced the entire agent subcommand tree with unified profile commands: cloister status shows everything (both backends, VM state, gateway health), cloister logs tails the gateway log, cloister stop stops the VM. One interface, regardless of what’s running underneath.

The old cloister agent command now prints a polite deprecation message pointing to the new commands. Because breaking people’s muscle memory without a breadcrumb trail is rude.

What I Learned (Again)

When I hit the Docker networking problems, my instinct was to add complexity. A proxy for the CSP. A flag to disable auth. A bind override. Each solution was individually defensible. Together, they formed a tower of compensating controls that existed only because the foundational decision — running a macOS-native application inside Docker inside Linux inside macOS — was wrong.

The Lume approach doesn’t need any of those fixes because it doesn’t create any of those problems.

Removing abstraction layers is harder than adding them. You have to admit the layer you built was wrong. You have to resist the voice that says “but it works fine with the three workarounds.” It does work fine. But when you’re building infrastructure that autonomous AI agents run on, “fine” is measured in security bypasses you hope nobody exploits.

The setup wizard is the part I’m most satisfied with. Not because the code is clever — it’s mostly prompts, SSH commands, and state tracking — but because it encodes every gotcha, every undocumented field name, every TTY password issue, every keychain drift problem into a flow that takes 3 minutes instead of a day. The next person who wants to run OpenClaw in an isolated macOS VM doesn’t need to learn any of this. They just run one command.

That’s what infrastructure is supposed to do: absorb complexity so the user doesn’t have to.

GitHub logo

ekovshilovsky
/
cloister

Run multiple Claude Code accounts & AI agents on one Mac — isolated macOS VMs with secure sandboxing

Cloister: Isolated VMs for AI Agents & Multi-Account Claude Code on macOS

CI
Release
Go Report Card
License: MIT

Isolated macOS VM environments for running multiple Claude Code organizations, sandboxing autonomous AI agents like OpenClaw, and separating credentials across client engagements.

cloister demo

Why cloister?

Multi-account isolation. Claude Code stores credentials, conversation history, and project config in ~/.claude. If you work across multiple organizations, every session shares the same identity. cloister gives each account its own isolated VM with separate credentials, CLAUDE.md, and conversation history.

Autonomous agent containment. AI agents like OpenClaw run 24/7 with shell access, browser control, and cron scheduling. cloister’s VM isolation is stronger than Docker (separate kernel, not just namespace isolation) — services inside the VM are unreachable from the host unless explicitly tunneled, and cloister stop is an instant kill switch.

Dual-backend architecture. Colima (Linux VMs) for Claude Code isolation and Docker workloads. Lume (macOS VMs via Apple Virtualization Framework) for OpenClaw…

cloister — Isolated VM environments for AI coding agents and multi-account separation. Dual-backend: Colima (Linux) and Lume (macOS). One-command OpenClaw setup.

op-forward — Forward 1Password CLI across SSH boundaries with biometric authentication.

5 Alternatives to OpenClaw If You Need Enterprise-Grade Security

OpenClaw is impressive. It’s also not built for teams with SSO requirements, audit logs and data isolation mandates. Here’s what to use instead.

Let me save you three weeks of evaluation time.

You heard about OpenClaw. You watched the demo videos. A local AI agent framework that runs entirely on your infrastructure, no data leaving your environment, full tool use, open source, it looked like exactly what your team needed. Then someone from your security team asked a few questions and the enthusiasm in the room dropped noticeably.

Where are the audit logs? How does it handle SSO? What’s the data isolation model between users? Can it integrate with our existing RBAC implementation? Is there a SOC 2 report?

These are not unreasonable questions. They’re the standard questions that any enterprise security team asks about any new piece of infrastructure. And the honest answer for OpenClaw in its current form is: these capabilities either don’t exist, require significant custom engineering to implement, or are on the roadmap without committed timelines.

This is not a criticism of OpenClaw. It’s a framework built by developers for developers, optimised for capability demonstration and local deployment flexibility. It does what it says on the tin. What it says on the tin is not “enterprise-grade security-compliant agent infrastructure.”

If you need that, here’s what to evaluate instead, with an honest comparison of where each option fits.

What “Enterprise-Grade Security” Actually Means Here

Before the alternatives, it helps to be precise about what we’re evaluating against. When enterprise security teams evaluate agent platforms, the requirements cluster around five areas:

Authentication and access control — SSO integration (SAML 2.0, OIDC), MFA enforcement, role-based access control with granular permissions, service account management.

Audit logging — immutable, queryable logs of every agent action, tool call, data access and user interaction. Exportable to SIEM systems. Retention policies that meet compliance requirements.

Data isolation — tenant-level data separation, configurable data residency, encryption at rest and in transit with customer-managed keys where required.

Security compliance — SOC 2 Type II certification (or equivalent), penetration testing recency, vulnerability disclosure programme, SLA for security patches.

Network and deployment security — VPC deployment support, private endpoints, air-gapped deployment options for highly regulated environments.

OpenClaw scores poorly on most of these out of the box. The alternatives below score well on most of them, with different trade-offs in capability, cost and deployment complexity.

For a detailed technical breakdown of what OpenClaw is and how its self-hosted architecture works before you make a comparison decision, the Dextra Labs explainer covers the architecture in detail.

The Comparison Table

Now let’s go through each one properly.

1. LangChain Enterprise (LangSmith + LangServe)

Best for: Teams already using LangChain who need to move from prototype to production with audit capability
LangChain’s open-source framework is where most teams start building agents. LangChain Enterprise adds the production and compliance layer on top, LangSmith for observability, tracing and audit logging, LangServe for deployment with authentication and the enterprise support tier for the compliance documentation your procurement team needs.

The security story:
LangSmith’s tracing infrastructure captures every LLM call, tool invocation, input, output and intermediate step in a queryable audit log. For a compliance team asking “what did your AI agent do and why,” LangSmith gives you the evidence trail. SSO via SAML 2.0 and OIDC is supported. Role-based access control is granular enough for most enterprise requirements.

pythonfrom langchain import hub
from langchain.agents import AgentExecutor, create_react_agent
from langchain_anthropic import ChatAnthropic
from langsmith import Client

import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your_langsmith_key"
os.environ["LANGCHAIN_PROJECT"] = "enterprise-agent-prod"

# Every action below is automatically traced
# and logged to LangSmith with full audit trail
llm = ChatAnthropic(model="claude-sonnet-4-5")

tools = [search_tool, calculator_tool, db_query_tool]
prompt = hub.pull("hwchase17/react")

agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent, 
    tools=tools,
    verbose=True,
    # Return intermediate steps for audit log
    return_intermediate_steps=True,
    # Handle parsing errors gracefully
    handle_parsing_errors=True
)

result = agent_executor.invoke({
    "input": "Analyse Q3 customer churn and identify top 3 factors"
})

# Full trace available in LangSmith dashboard
# including every tool call and LLM reasoning step

Where it falls short: Self-hosting LangSmith Enterprise is possible but operationally complex. If your requirement is air-gapped deployment with no external dependencies, the setup effort is significant. The open-source LangSmith self-hosted version exists but lacks the enterprise security features.

Pricing: Contact sales. Roughly $500-2000/month depending on trace volume and team size.

2. Google Vertex AI Agent Builder

Best for: Teams on GCP who need the strongest possible compliance posture and are comfortable with a managed service
Vertex AI’s agent infrastructure inherits Google Cloud’s compliance certifications, SOC 2, ISO 27001, HIPAA, FedRAMP (at higher tiers) and its security model. If your compliance requirements include any of the major certifications and you’re already on GCP, this is the path of least resistance.

The security story:
Data never leaves your GCP project. VPC Service Controls can isolate your agent infrastructure from the public internet entirely. Cloud Audit Logs captures every API call with immutable records exportable to Chronicle or your SIEM. IAM integration handles SSO, MFA and fine-grained permissions via Google Workspace or your external IdP.

pythonfrom google.cloud import aiplatform
from vertexai.preview import reasoning_engines

# Initialize with your project's security configuration
aiplatform.init(
    project="your-gcp-project",
    location="us-central1",
    # All data stays within your GCP project
    # VPC SC controls apply automatically
)

# Define agent with tool use
class EnterpriseAgent(reasoning_engines.Queryable):
    def query(self, input: str) -> str:
        # Agent logic here
        # All interactions logged to Cloud Audit Logs
        pass

# Deploy to managed infrastructure
# Inherits GCP SOC 2 / ISO 27001 compliance
agent = reasoning_engines.ReasoningEngine.create(
    EnterpriseAgent(),
    requirements=["anthropic>=0.20.0"],
    display_name="enterprise-agent-prod"
)

response = agent.query(input="Summarise open support tickets")

Where it falls short: GCP lock-in is real. If your infrastructure is multi-cloud or on-premises, Vertex AI’s compliance story doesn’t extend outside GCP. The agent builder is also less flexible than open frameworks, custom tool integrations require more boilerplate.

Pricing: Usage-based. Typically $0.025-0.05 per 1K characters for the agent layer, plus underlying model costs.

3. Microsoft AutoGen Enterprise

Best for: Teams in Microsoft-heavy environments, Azure, M365, Entra ID, who need multi-agent workflows with enterprise auth
AutoGen’s multi-agent framework is genuinely powerful for complex workflows where multiple specialised agents collaborate. The enterprise version adds Azure AD (Entra ID) integration, which means SSO is essentially free if you’re already on the Microsoft stack and audit logging via Azure Monitor.

The security story:
Entra ID integration handles authentication, MFA and conditional access policies automatically. Azure Monitor captures agent interactions with Log Analytics workspaces for compliance reporting. Data residency is configurable to your Azure region. SOC 2 and ISO 27001 coverage through Azure’s compliance certifications.

pythonimport autogen
from autogen import AssistantAgent, UserProxyAgent, GroupChat

# Configure with Azure OpenAI or Anthropic via Azure
# All traffic stays within your Azure tenant
config_list = [
    {
        "model": "gpt-4o",
        "api_type": "azure",
        "api_base": "https://your-resource.openai.azure.com/",
        "api_version": "2024-02-01",
        # Auth handled by Azure AD managed identity
        # No secrets in config
        "azure_ad_token_provider": "DEFAULT"
    }
]

# Define specialised agents for enterprise workflow
code_reviewer = AssistantAgent(
    name="code_reviewer",
    system_message="Review code for security vulnerabilities and standards compliance.",
    llm_config={"config_list": config_list}
)

security_auditor = AssistantAgent(
    name="security_auditor", 
    system_message="Assess security posture and compliance requirements.",
    llm_config={"config_list": config_list}
)

orchestrator = UserProxyAgent(
    name="orchestrator",
    human_input_mode="NEVER",
    code_execution_config={
        "work_dir": "sandbox",
        "use_docker": True  # Isolated execution
    }
)

# Multi-agent workflow with full Azure Monitor logging
groupchat = GroupChat(
    agents=[orchestrator, code_reviewer, security_auditor],
    messages=[],
    max_round=10
)

Where it falls short: AutoGen’s enterprise features are most complete when you’re using Azure OpenAI as the model provider. Using Anthropic or other providers requires more configuration and you lose some of the native Azure security integration benefits.

Pricing: AutoGen framework is open source. Enterprise support and Azure infrastructure costs apply separately.

4. Haystack Enterprise (deepset)

Best for: Teams building document-heavy agent workflows, RAG pipelines, document processing, knowledge base agents, who need self-hosting with a strong security model

Haystack is the most genuinely open-source option on this list from a security architecture perspective. deepset’s enterprise offering adds the compliance layer on top of the open-source framework, but the framework itself is auditable in a way that matters to security teams who need to understand what their software is doing.

The security story:

Haystack Enterprise offers self-hosted deployment with full data isolation, your data never touches deepset’s infrastructure unless you choose the cloud offering. SSO via SAML 2.0 and OIDC. Audit logging at the pipeline level. SOC 2 Type II certified. The architecture is pipeline-based, which makes it easier to implement and audit data flow controls than more opaque agent frameworks.

pythonfrom haystack import Pipeline
from haystack.components.generators import AnthropicGenerator
from haystack.components.retrievers import InMemoryBMRetriever
from haystack.components.builders import RAGPromptBuilder
from haystack.logging import configure_logging

# Configure enterprise logging
# Outputs to your SIEM via structured JSON
configure_logging(
    level="INFO",
    format="json",
    include_metadata=True,     # Captures user context
    include_component_trace=True,  # Full pipeline trace
    output="stdout"            # Capture by your log aggregator
)

# Build auditable RAG agent pipeline
pipeline = Pipeline()

pipeline.add_component(
    "retriever", 
    InMemoryBMRetriever(document_store=document_store)
)
pipeline.add_component(
    "prompt_builder",
    RAGPromptBuilder(template=enterprise_template)
)
pipeline.add_component(
    "generator",
    AnthropicGenerator(model="claude-sonnet-4-5")
)

# Connect components — data flow is explicit and auditable
pipeline.connect("retriever.documents", "prompt_builder.documents")
pipeline.connect("prompt_builder.prompt", "generator.prompt")

# Run with user context for audit trail
result = pipeline.run(
    data={
        "retriever": {"query": user_query},
        "prompt_builder": {
            "query": user_query,
            "user_id": current_user.id,  # Audit context
            "session_id": session.id
        }
    }
)

Where it falls short: Haystack is optimised for pipeline-based workflows. If your use case requires complex multi-step autonomous agent behaviour, the ReAct-style loops that OpenClaw excels at, Haystack requires more custom work to implement.

Pricing: Open source framework. Enterprise support and cloud offering via deepset starting around $1,500/month.

5. CrewAI Enterprise

Best for: Teams who need multi-agent workflows with role-based agents and want the fastest path from prototype to production
CrewAI’s framework is the newest on this list and the most rapidly developing. The enterprise offering is also the newest, SOC 2 Type II certification is in progress rather than complete, which is worth flagging for teams with strict compliance requirements. That said, the security architecture is solid and the certification is expected to complete in 2026.

The security story:
SSO via SAML 2.0 and OIDC. Comprehensive audit logging of every agent action and crew interaction. Tenant-level data isolation in the cloud offering. Self-hosting available with a private deployment option. The role-based agent model maps naturally to enterprise organisational structures, a manager agent, a researcher agent, a writer agent, which makes access control design more intuitive.

pythonfrom crewai import Agent, Crew, Task, Process
from crewai.security import AuditLogger, AccessControl

# Configure enterprise security
audit_logger = AuditLogger(
    output="structured_json",
    include_inputs=True,
    include_outputs=True,
    include_tool_calls=True,
    retention_days=90  # Configurable for compliance
)

access_control = AccessControl(
    rbac_config="./config/rbac.yaml",
    sso_provider="okta",  # Or AzureAD, Google, etc.
    mfa_required=True
)

# Define agents with role-based access
analyst = Agent(
    role="Senior Data Analyst",
    goal="Analyse customer data and identify patterns",
    backstory="Expert in customer behaviour analysis",
    verbose=True,
    tools=[db_query_tool, analytics_tool],
    # Scoped tool access per role
    allowed_tools=["db_query", "analytics"],
    max_iter=5
)

reporter = Agent(
    role="Report Writer",
    goal="Produce executive-ready summaries",
    backstory="Specialises in clear business communication",
    tools=[document_tool],
    allowed_tools=["document_write"]
)

# Tasks with explicit data handling
analysis_task = Task(
    description="Analyse Q3 churn data. Focus on enterprise segment.",
    agent=analyst,
    expected_output="Structured analysis with top 5 churn factors",
    # Data classification for audit
    data_classification="confidential"
)

crew = Crew(
    agents=[analyst, reporter],
    tasks=[analysis_task, report_task],
    process=Process.sequential,
    audit_logger=audit_logger,
    access_control=access_control
)

result = crew.kickoff()

Where it falls short: The SOC 2 gap is real for teams with strict compliance timelines. If you need certification documentation for a procurement decision this quarter, CrewAI Enterprise isn’t ready yet. Check back in six months.
Pricing: Enterprise pricing on request. Community edition is open source and free.

How to Choose

The honest decision framework comes down to three questions.

What’s your infrastructure constraint? GCP-locked teams should look at Vertex AI first, the compliance story is the strongest and the integration with existing GCP security tooling is seamless. Azure-heavy teams should look at AutoGen Enterprise. Infra-agnostic teams with self-hosting requirements should look at LangChain Enterprise or Haystack Enterprise.

What’s your workflow type? Document-heavy RAG pipelines, Haystack. Multi-agent collaborative workflows, AutoGen or CrewAI. General-purpose agent with strong observability, LangChain Enterprise. Maximum compliance certification coverage, Vertex AI.

What’s your timeline? If you need SOC 2 documentation for a procurement decision this quarter, CrewAI is out. If you can wait six months and the workflow fit is strong, it’s worth reconsidering.

None of these are perfect OpenClaw replacements for the developer who wants full local control with zero external dependencies. That’s genuinely a trade-off, the security features that enterprise teams require add architectural complexity that pure local deployment can’t accommodate.

If you’re still evaluating whether OpenClaw might work for your use case with some custom security engineering or whether one of these alternatives is the right fit, the https://dextralabs.com/blog/top-openclaw-alternatives/ comparison from Dextra Labs covers the detailed feature matrix, pricing and deployment complexity for each option.

If you’re evaluating self-hosted agent platforms more broadly, Dextra Labs has a detailed breakdown of OpenClaw’s architecture and where it fits in the market including the specific security gaps and what it would take to close them with custom engineering.

Copilot CLI Weekly: MCP Servers Get LLM Access

MCP Sampling Lands in v1.0.13-0

The most significant change this week is buried in a prerelease tag: MCP servers can now request LLM inference. Version 1.0.13-0, released today, adds sampling support to the Model Context Protocol implementation. MCP servers can call the user’s LLM through a permission prompt, eliminating the need for servers to maintain their own API subscriptions.

This is a shift in how MCP servers work. Before this, an MCP server was a tool provider — it exposed functions the agent could call, but it couldn’t reason on its own. Now, with sampling, an MCP server can delegate reasoning back to the user’s LLM mid-execution. A recipe generator can ask the LLM to format output. A code analysis server can ask for natural language summaries. The user approves each request via a review prompt, maintaining control over what their LLM processes.

The feature has been in the MCP spec since VS Code shipped it last summer, but adoption has been slow. Copilot CLI supporting it means the entire GitHub-integrated toolchain now has access to this capability. If you’re building MCP servers, you can now lean on the user’s model instead of spinning up your own inference backend.

Model Picker Gets a Full Redesign

Version 1.0.12 landed March 26 with 28 improvements and fixes, but the UX standout is the full-screen model picker with inline reasoning effort controls. Previously, selecting a model and adjusting reasoning effort were separate steps. Now, the picker opens in full-screen mode, and you adjust reasoning effort with arrow keys (← / →) while browsing models.

The picker also reorganizes models into three tabs: Available, Blocked/Disabled, and Upgrade. This solves the confusion around which models are accessible based on your plan and organization policy. If you’re on a free tier and wondering why you can’t select opus-4.6, the picker now tells you explicitly instead of silently blocking the selection.

The reasoning effort level also displays in the header next to the model name (e.g., claude-sonnet-4.5 (high)), so you always know your current configuration without running a command.

Organization Policy Enforcement and Memory Fixes

v1.0.12 also included critical stability work. The CLI no longer crashes with out-of-memory errors when shell commands produce high-volume output — a real problem when running builds or test suites that generate megabytes of logs. Memory usage improvements extend to the grep tool, which now handles large files without exhausting available RAM.

Version 1.0.11, released March 23, brought organization-level policy enforcement for third-party MCP servers. If your organization uses an MCP allowlist, that policy now applies universally. Blocked servers no longer show up in /mcp show, and the CLI displays a warning when policy blocks a server from loading. This matters for enterprise deployments where security teams need to audit what external tools can access organizational data.

Skills Directory Alignment and Hook Improvements

v1.0.11 also aligned the personal skills directory with VS Code’s GitHub Copilot for Agents extension. The CLI now discovers skills in ~/.agents/skills/, matching the default used by the VS Code extension. If you’ve been maintaining separate skill directories for the CLI and the extension, you can consolidate them.

Extension hooks from multiple extensions now merge instead of overwriting each other. Previously, if two extensions defined a sessionStart hook, only one would fire. Now, both execute, and the additionalContext from sessionStart hooks is injected into the conversation. That’s critical for building custom agents that layer multiple extension behaviors.

The /yolo Command Gets More Precise

The /allow-all command (aliased as /yolo) now supports subcommands: /yolo on, /yolo off, and /yolo show. This replaces the previous toggle behavior with explicit enable/disable semantics, reducing the risk of accidentally leaving permission-free mode enabled. The CLI also persists /yolo path permissions across /clear session resets, so you don’t have to re-approve directories after clearing context.

What Else Shipped

The other notable fixes from v1.0.12:

  • Workspace MCP servers defined in .mcp.json now load correctly when your working directory is the git root
  • Sessions with active work are no longer cleaned up by the stale session reaper (a frustrating bug if you left a session idle mid-task)
  • Resume session now correctly restores your previously selected custom agent
  • Clipboard copy works on Windows even when a non-system clip.exe is in PATH
  • Emoji selection in the terminal now works correctly (yes, this matters)

Version 1.0.13-0 added a handful of additional fixes, including correct reasoning effort handling for Bring Your Own Model (BYOM) providers and better error messaging when using classic Personal Access Tokens.

The Platform Is Taking Shape

Three releases in seven days, with one major capability (MCP sampling), a redesigned model picker, enterprise policy enforcement, and dozens of stability fixes. The pattern I’ve been tracking since the biggest week yet continues: this isn’t a standalone tool anymore. It’s a platform. MCP servers can now request inference, extensions can inject context into subagents, and the SDK keeps expanding with hooks and custom commands.

The CLI is becoming the runtime for a growing ecosystem of agent tooling. If you’re building developer tools and haven’t looked at integrating with it, this is the week that makes the case.