Get Your 1.6 Seconds Back - What AI Gets Wrong With MongoDB

We keep seeing AI put createIndex where it doesn't belong. At the top of the server file, one await per index, sitting right above app.listen, or worse, inside a route handler where it runs on every single request. Sometimes that code makes it to production, and when it does, MongoDB gets accused of being slow. So we measured what the habit actually costs.

createIndex is idempotent, so when the index already exists the server builds nothing and just says so. That's why this code survives review, it works. But every no-op is still a full round trip, and the awaits are serial. Here's the pattern, then the measurements at real scale.

The label below is the prompt we gave the AI to generate the block.

How to create MongoDB indexes at the top of an express server file before the routes.

Bad:

const app = express();

await db.collection('products').createIndex({ name: 1 });
await db.collection('products').createIndex({ category: 1, price: -1 });
await db.collection('users').createIndex({ email: 1 }, { unique: true });
await db.collection('orders').createIndex({ userId: 1, createdAt: -1 });
await db.collection('sessions').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });

app.get('/search', async (req, res) => {
  const results = await db.collection('products')
    .find({ name: req.query.q })
    .toArray();
  res.json(results);
});

app.listen(3000);

Five indexes is a toy. A real app has more, so we built a realistic 30 collection commerce schema with 69 indexes, unique lookups, compound list-and-sort pairs, five TTLs, two sparse, one partial, one text index, and ran six boot strategies against both environments. Every collection held zero documents the whole time, so nothing ever got built. We were timing pure no-op round trips, which is exactly what your boot pays.

Local Development Production
MongoDB 8.0 in Docker, same machine
ping 1.07 ms

First the steady state. Indexes exist, connection pool warm. This is what a boot pays once connections are reused:

strategy what it is Local Production
serial 69 sequential await createIndex 0.14 s (135 ms) 1.63 s (1629 ms)
batched serial 30 sequential createIndexes 0.05 s (54 ms) 0.62 s (617 ms)
parallel all 69 in one Promise.all 0.03 s (25 ms) 0.23 s (225 ms)
pool of 8 69 calls, 8 in flight 0.05 s (47 ms) 0.19 s (193 ms)
pool of 16 69 calls, 16 in flight 0.05 s (48 ms) 0.11 s (106 ms)
batched + parallel 30 createIndexes in one Promise.all 0.02 s (16 ms) 0.03 s (30 ms)

The AI version spends 1.63 s (1629 ms) of every production boot confirming 69 indexes that already exist. The winner clears the entire schema in 0.03 s (30 ms), about one and a half pings for 69 index specs. And notice plain Promise.all is not the fix people think it is. All 69 calls fired at once still costs 0.23 s (225 ms), seven times the winner.

Steady state flatters everyone though, because a real boot starts from nothing. Container start, serverless cold start, plain node server.js. So we also ran each strategy as 7 independent processes, start node, connect, create, exit:

strategy index time total with connect
serial 1.62 s (1619 ms) 2.08 s (2080 ms)
batched serial 0.65 s (652 ms) 1.11 s (1110 ms)
one chain per collection 0.61 s (615 ms) 1.29 s (1288 ms)
parallel 0.57 s (571 ms) 1.28 s (1283 ms)
pool of 16 1.01 s (1006 ms) 1.42 s (1421 ms)
batched + parallel 0.39 s (392 ms) 0.82 s (819 ms)

Both tables side by side, plus the option the benchmark could not run, doing no index work at boot at all:

warm steady state cold fresh process
serial at boot, the AI version 1.63 s (1629 ms)
batched + parallel at boot 0.03 s (30 ms)
script instead, index work at boot 0 s
how much slower the AI version boots 1.6 s

The bottom two rows are the actual claim of this post. The fix is not a faster way to run indexes at boot, and every strategy in these tables is still the wrong place for the work. The fix is a separate script, which makes the app's index cost at boot zero, so the AI version boots 1.6 seconds slower than the script version. And warm or cold barely matters, serial was never using more than one connection, so it pays nearly the same either way. The batched numbers still earn their place for one reason, the script is itself a fresh process, so 0.39 s (392 ms) of index time, 0.82 s (819 ms) wall clock with connect, is exactly what node db/indexes.js costs on the day an index actually changes. That's the whole trade. 1.6 s off every single boot, paid back as 0.8 s once per index change.

The fresh process run also flipped one ranking. Pool of 16 was second best warm and second worst cold, because capping concurrency starves a cold pool of the parallelism it needs to warm up. A tuning choice that looks good in a benchmark loop can be the wrong one at the moment that matters.

The reason this mistake keeps shipping is in the next table. Same questions, answered by each environment:

question Local Development says Production says
cost of the AI serial boot 0.14 s (135 ms), invisible 1.63 s (1629 ms), a visible stall
is plain Promise.all good enough yes, 1.6x off the best no, 7.4x off the best
spread between all six strategies, fresh process 0.09 to 0.18 s (90 to 176 ms), everything within 2x 0.39 to 1.62 s (392 to 1618 ms), a 4.1x spread

The last row is the point. On a laptop every strategy lands inside the noise, so any ranking formed there is meaningless, including the one that says this doesn't matter. The decision is only visible in production, which is exactly where nobody is looking when the AI writes the code.

We also went in with a theory about why 69 parallel calls lose, and the data killed it. The guess was same-collection contention, products takes three index calls at once, they must be colliding. So we ran a collision-free arm, 30 chains, one per collection, same 69 commands:

arm commands in flight same-collection collisions Production
all 69 in Promise.all 69 69 yes 0.20 s (203 ms)
30 chains, one per collection 69 30 no 0.10 s (97 ms)
pool of 16 69 16 yes 0.11 s (106 ms)
batched + parallel 30 30 no 0.03 s (30 ms)

Collision-free at 30 in flight and collision-allowed at 16 in flight cost the same, so contention is not the mechanism. What the numbers actually support is two independent levers. Command count dominates, 30 commands land at 1.5 pings while 69 commands sit around 5 pings no matter how sensibly you schedule them. And concurrency stops paying above roughly 16 to 30 in flight, unbounded Promise.all is on the wrong side of that curve. The winner pulls both levers at once, batch per collection, then Promise.all the collections.

For completeness, the first deploy, where the 69 indexes genuinely don't exist and really get built:

strategy Production
serial 2.17 s (2165 ms)
batched + parallel 0.57 s (566 ms)
pool of 16 0.50 s (502 ms)

The spread compresses because actual creation work dominates instead of round trips. It gets paid once. The no-op tables above get paid on every boot, forever, which is why they're the story.

And none of it belongs in your boot at all. The tables show what AI-written startup code costs today, and how the index script should be written so it's fast on the day you do run it:

How to batch MongoDB index creation into a standalone script using createIndexes per collection in parallel.

Good:

// db/indexes.js - never imported by the app. Run it when an index changes: node db/indexes.js
await Promise.all([
  db.collection('products').createIndexes([
    { key: { name: 1 } },
    { key: { category: 1, price: -1 } }
  ]),
  db.collection('users').createIndexes([
    { key: { email: 1 }, unique: true }
  ]),
  db.collection('orders').createIndexes([
    { key: { userId: 1, createdAt: -1 } }
  ]),
  db.collection('sessions').createIndexes([
    { key: { expiresAt: 1 }, expireAfterSeconds: 0 }
  ])
]);
console.log('indexes ready');
process.exit(0);

And server.js has no index code anywhere:

// server.js
app.get('/search', async (req, res) => {
  const results = await db.collection('products').aggregate([
    { $match: { name: String(req.query.q) } },
    { $limit: 20 },
    { $project: { name: 1, price: 1, description: 1 } }
  ]).toArray();
  res.json(results);
});
don't do
69 commands, one at a time 30 commands, all at once
1.63 s (1629 ms) on every boot 0 s at boot, 0.82 s (819 ms) script run when an index changes

Two warnings if you re-run any of this, both earned the hard way. The first is that warmup is load-bearing. Measured with no warmup passes, the winner reads 0.18 s (181 ms) instead of 0.03 s (30 ms), six times too high, and the raw samples just keep falling, 348, 291, 1224, 181, 48, 44, 34, which is a connection pool warming up in front of the timer. Skip warmup and the numbers come out wrong, and possibly the ranking too. Our quoted numbers are medians of 15 runs after 5 discarded warmup passes, strategies interleaved so host variance spreads evenly.

The second is that we crashed a MongoDB container twice getting here. The first design gave each of the six strategies its own private 30 collections, and WiredTiger keeps a file per collection and per index, so 180 collections, around 410 indexes and 69 concurrent connections blew straight through the container's limit of 1024 open files. Panic, then a segfault on the retry. The fix was sharing one set of collections across arms, which is sound because warm no-ops mutate nothing, and raising the file limit to 64000. That one matters outside the benchmark too. File descriptors scale with collections times indexes times connections, and 1024 is not enough for a 30 collection app booting in parallel.

Two closing failure modes that no benchmark captures, because they only fire once. The route handler version, createIndex inside the endpoint itself, looks free for the same no-op reason, but point it at a fresh environment or a collection restored without its indexes and the first request starts a real index build that reads every document in the collection. Every request behind it issues the same createIndex, sees that exact build already in progress, and waits. The endpoint is down for the entire build and not a single error is thrown.

And the quiet one. Change an index's keys in code without setting an explicit name and you don't update the index, you create a second one, because the default name changes with the keys. The old index stays behind, taxing every write until someone audits the collection. A single script that lists every index you own is where you catch that. Sixty-nine createIndex calls scattered around a codebase is where you don't.

reddit.com
u/TimAtMongoDB — 1 day ago

$skip vs Keyset Pagination - What AI Gets Wrong With MongoDB

Ask any AI how to paginate a MongoDB collection and you get $skip. Every single time, unless you put the word keyset in the prompt.

I ran explain() on a million product documents to see what that actually costs. Page 500 examines 10,000 documents to return 20. Page 5000 examines 100,000. The keyset version examines 20 no matter which page you ask for.

The labels below are the prompts I gave the AI to generate each block.

How to paginate MongoDB query results using skip and limit by page number.

Bad:

const PAGE = 2;
const PAGE_SIZE = 20;
const RESULTS = await db.collection('products')
  .find({})
  .skip(PAGE * PAGE_SIZE)
  .limit(PAGE_SIZE)
  .toArray();

How to implement MongoDB keyset pagination using last seen document id with aggregation pipeline.

Good:

const PAGE_SIZE = 20;
let LAST_SEEN_ID = null;
try {
  const RESULTS = await db.collection('products').aggregate([
    ...(LAST_SEEN_ID ? [{$match:{_id:{$gt:LAST_SEEN_ID}}}] : []),
    {$sort:{_id:1}},
    {$limit:PAGE_SIZE},
    {$project:{name:1,sku:1,category:1,main_image:1}}
  ]).toArray();
  LAST_SEEN_ID = RESULTS.at(-1)?._id ?? LAST_SEEN_ID;
} catch (e) {
  console.error(e.message);
}

How to build MongoDB pagination with page cache supporting forward, backward, and direct page jumps.

Perfect:

const PAGE_SIZE = 20;
const PAGE_CACHE = new Map();

async function getPage(pageNum) {
  if (pageNum < 1) throw new Error('pageNum must be >= 1');

  const prevPage = PAGE_CACHE.get(pageNum - 1);
  const currPage = PAGE_CACHE.get(pageNum);

  const seek = currPage?.firstId ? {$match:{_id:{$gte:currPage.firstId}}}
              : prevPage?.lastSeenId ? {$match:{_id:{$gt:prevPage.lastSeenId}}}
              : pageNum>1 ? {$skip:(pageNum-1)*PAGE_SIZE}
              : null;

  const raw = await db.collection('products').aggregate([
    seek,
    {$sort:{_id:1}},
    {$limit:PAGE_SIZE + 1 },
    {$project:{name:1,sku:1,category:1,main_image:1}}
  ].filter(Boolean)).toArray();

  const hasNext = raw.length > PAGE_SIZE;
  const results = hasNext ? raw.slice(0, PAGE_SIZE) : raw;

  if (results.length > 0) {
    PAGE_CACHE.set(pageNum, {firstId:results[0]._id,lastSeenId:results.at(-1)._id});
  }

  return {results,hasPrev:pageNum > 1,hasNext};
}

try {
  const [TOTAL_DOCUMENTS, P1] = await Promise.all([
    db.collection('products').estimatedDocumentCount(),
    getPage(1)
  ]);
  const TOTAL_PAGES = Math.ceil(TOTAL_DOCUMENTS / PAGE_SIZE);
} catch (e) {
  console.error(e.message);
}

Bad is 0-indexed, so PAGE = 2 actually hands you the third page. Perfect counts from 1. AI flips between the two without ever telling you which one it picked.

$skip scans and discards every document before your page. It also breaks under concurrent writes. A new document inserted on page 2 shifts everything after it, so page 3 shows the same document twice or skips one entirely, and nothing errors.

Good is keyset. Constant time wherever you are, but forward only. Perfect adds a page cache so you can go forward, backward, and jump straight to any page number. It still falls back to $skip for a cold jump, then caches that position on the way through so it never pays for it twice.

The cache is the same LAST_SEEN_ID from the Good example, stored per page instead of in one variable. One variable only remembers where you stopped, which is the whole reason Good can't go backward. Remember the first and last _id of every page you've been to and you can land on any of them directly.

Keyset is only fast if the field you sort on is indexed. _id is indexed automatically and that index can't be dropped, so the examples above need no setup at all. Point the same pattern at created_at without adding an index and you get a COLLSCAN, 10,020 documents examined instead of 20. You moved the scan, you didn't remove it. A single field index works in both sort directions, while a compound index has to match the sort direction on every field or be its exact inverse.

getPage isn't just paginating, it's fetching what the screen renders. A product grid needs a name, a sku, a category and a thumbnail. It does not need the description, the variants array, the spec sheet or the eight other image URLs. A page of 20 full product documents came back at 45,324 bytes on my test catalog. Projected down to those four fields, 3,362. That is what crosses the network and sits in your app memory on every request, and AI hands you the whole document every time.

Put $project after $limit so you only shape the 20 documents you're keeping. _id comes back whether you list it or not, which matters because the keyset needs it.

Sorting by _id means sorting by creation time, because an ObjectId starts with a 4 byte timestamp. You can pull it back out with _id.getTimestamp(). Keep a created_at field anyway, querying by Date beats building an ObjectId every time you need a range. ObjectIds made in the same second on different servers have no guaranteed order between them either.

estimatedDocumentCount() is called estimated for a reason. It reads collection metadata instead of counting, which is why it came back in 1.3ms where countDocuments({}) took 202ms on the same million documents. It drifts after an unclean shutdown and it counts orphans on a sharded cluster. Fine for a page count, not for anything that has to be exact.

reddit.com
u/TimAtMongoDB — 3 days ago

Cursors vs .toArray() - What AI Gets Wrong With MongoDB

AI almost always reaches for toArray() before doing any work on your data. Most training examples are out-of-context snippets, so it doesn't know better. toArray() holds your entire result set in RAM before you can touch a single document. With a cursor, the driver fetches in batches. Processed documents get GC'd while the rest stream in.

How to fetch all active users from MongoDB and send emails using find and toArray.

**Bad**

const users = await db.collection('users')
.find({ active: true })
.toArray();

users.forEach(async (user) => {
await sendEmail(user);
});

How to stream MongoDB documents with a cursor using for await to process each document without loading all into memory.

**Good:**

const cursor = db.collection('users').aggregate([
{ $match: { active: true } }
]);

for await (const user of cursor) {
await sendEmail(user);
}

How to process MongoDB cursor results concurrently with a concurrency limit without blocking the async loop.

**Perfect:**

function executor(limit) {
let running = 0
const queue = []
const flush = () => {
while (running < limit && queue.length) {
running++
queue.shift()().finally(() => { running--; flush() })
}
}
return fn => { queue.push(fn); flush() }
}

const add = executor(10);
const cursor = db.collection('users').aggregate([
{ $match: { active: true } }
]);

for await (const user of cursor) {
add(() => sendEmail(user))
};

Bad loads everything into RAM then serializes. Good streams documents but still sends one email at a time. Perfect streams AND fires up to 10 emails concurrently without the loop ever waiting.

**Bonus:** A cursor with `for await` only makes sense when you're doing work per document. If you're just collecting into an array to send a response, use `.toArray()` directly. Wrapping `.toArray()` in a `for await` loop buys you nothing.

reddit.com
u/TimAtMongoDB — 4 days ago

Cursors vs .toArray() - What AI Gets Wrong With MongoDB

AI almost always reaches for toArray() before doing any work on your data. Most training examples are out-of-context snippets, so it doesn't know better. toArray() holds your entire result set in RAM before you can touch a single document. With a cursor, the driver fetches in batches. Processed documents get GC'd while the rest stream in.

How to fetch all active users from MongoDB and send emails using find and toArray.

Bad

            const users = await db.collection('users')
              .find({ active: true })
              .toArray();
            
            users.forEach(async (user) =&gt; {
              await sendEmail(user);
            });

How to stream MongoDB documents with a cursor using for await to process each document without loading all into memory.

Good:

            const cursor = db.collection('users').aggregate([
              { $match: { active: true } }
            ]);
    
            for await (const user of cursor) {
              await sendEmail(user);
            }

How to process MongoDB cursor results concurrently with a concurrency limit without blocking the async loop.

Perfect:

            function executor(limit) {
              let running = 0
              const queue = []
              const flush = () =&gt; {
                while (running &lt; limit &amp;&amp; queue.length) {
                  running++
                  queue.shift()().finally(() =&gt; { running--; flush() })
                }
              }
              return fn =&gt; { queue.push(fn); flush() }
            }
    
            const add = executor(10);
            const cursor = db.collection('users').aggregate([
              { $match: { active: true } }
            ]);
    
            for await (const user of cursor) {
              add(() =&gt; sendEmail(user))
            };

Bad loads everything into RAM then serializes. Good streams documents but still sends one email at a time. Perfect streams AND fires up to 10 emails concurrently without the loop ever waiting.

Bonus: A cursor with for await only makes sense when you're doing work per document. If you're just collecting into an array to send a response, use .toArray() directly. Wrapping .toArray() in a for await loop buys you nothing.

reddit.com
u/TimAtMongoDB — 5 days ago
▲ 6 r/MongoDB_Official+1 crossposts

One MongoClient per App - What AI Gets Wrong With MongoDB

AI puts this in every route file it touches:

    const client = new MongoClient(uri);
    await client.connect();
    // query
    await client.close();

Looks clean. Problem is a MongoClient is a POOL, up to 100 connections. A free Atlas cluster takes 500 total. Five of these alive at once and your whole cluster budget is gone before one query runs. Then everyone blames Mongo for "randomly dying under load".

One client for the whole app. Create it once, import it everywhere. The driver does the pooling, that is literally its job.

Bonus AI also never gets right: Next.js dev SSR mode re-runs module scope on every hot reload. Your correct singleton turns into a new pool every time you hit save. Cache it on globalThis and it survives reloads:

    let client = globalThis._mongoClient;
    if (!client) {
      client = new MongoClient(process.env.MONGODB_URI);
      globalThis._mongoClient = client;
    }

Costs nothing in production, saves your dev cluster.

Grep your codebase for "new MongoClient". If you have more than one, you have a problem.

Make a module you import, and reuse so every script imports the same active connection.

    // db.js
    import { MongoClient } from 'mongodb';

    export const client = new MongoClient(process.env.MONGODB_URI, {
      appName: 'my-api',
    });

    export const db = client.db('app');
reddit.com
u/TimAtMongoDB — 4 days ago

You want us to show you what AI gets wrong with MongoDB queries?

We have a pile. Upvote if you want it, and drop the worst Mongo query an AI ever handed you.

reddit.com
u/TimAtMongoDB — 6 days ago

What do you actually want from a MongoDB community?

A few things we're considering:

  • Deep dives and best practices on schema design, aggregation, indexing, performance. The stuff that actually trips people up.
  • Office hours where you can bring a real problem
  • Discussion threads on how you're using specific features
  • Tutorials and how-tos from people who work on the product

Drop a comment. Everything here shapes what we build.

reddit.com
u/TimAtMongoDB — 20 days ago
▲ 29 r/MongoDB_Official+2 crossposts

Welcome to r/MongoDB_Official, here is what this sub is for

This is MongoDB's official subreddit, run by people who work on and with the database. We're excited you're here!

A quick note on what it is and is not, so you know what to expect.

What this sub is for:

  • Real help. Ask your MongoDB questions here. Modeling, aggregation, indexing, drivers, Atlas, vector search, performance, whatever you're stuck on. Questions aren't just tolerated, they're the point.
  • Learning. Tutorials, deep dives, and "how it actually works" posts, from the team and from you.
  • Announcements. Releases, features, and changes, so you hear it here first.
  • Showing your work. Built something on MongoDB? Post it, we want to see it.

What it is not:

  • A support ticket queue. For account or production-down issues, official support and the docs are still the right path. We will point you there when that is the better route.
  • A place for hype. We would rather be accurate than loud. If we get something wrong, tell us, and we will fix it.

A few ground rules: be kind, stay on topic, no spam, and no reposting the same self-promo. Beyond that, this is your sub as much as ours.

Watch this space over the next few weeks as we settle in, meet more of you, and shape this into the place you want it to be.

To kick it off: what are you building with MongoDB right now? We are reading every reply.

reddit.com
u/TimAtMongoDB — 17 days ago
▲ 7 r/claudeskills+1 crossposts

Follow up to my "pointless skill files" post. Half your skills should probably be rules.

After all the positive feedback I received on the skill posts I did a lot of research on current best practices on how to use Claude Code and this is what came out. BTW The best line in that thread wasn't mine: a good skill is a scar, not a resume. So I took the advice on and I'm writing a current, verified guide to the whole .claude system. This is the condensed version, the full frontmatter for each layer as it exists right now, plus the known issues for each. Tear it apart before I publish.

First, dates, because they explain a lot. All from the official changelog:

Oct 2025 (v2.0.20): skills launch. Dec 2025 (v2.0.64): .claude/rules/ added. Jan 2026 (v2.1.0): skills get context: fork, and skills show up in the slash menu by default. This is the point skills took over what custom commands did. Mar 2026 (v2.1.84): paths: frontmatter on rules and skills accepts a YAML list of globs.

Now ask Claude "should this be a skill or a rule". My Claude's training data ends in January. Rules were four weeks old at that point. Fork was one week old. Everything since, it has never seen. You get a confident answer from before the system existed in its current form. That goes for the frontmatter below too, Claude will happily invent keys or miss half of them.

RULES. The folder almost every kit skips, probably because it's the youngest, and the right home for most of what people ship as skills. One frontmatter field. That's the whole spec:

---
paths:              # the ONLY field. File globs. With it, loads only near
  - "src/db/**"     #   matching files, free otherwise. Without it, loads
  - "**/*.repo.ts"  #   every session, same cost as CLAUDE.md.
---
# MongoDB data-access rules
- Aggregation pipelines, never find().
- _id is an ObjectId, wrap incoming ids with new ObjectId(id).

Why a path rule beats a skill for knowledge, two things:

Cost. A skill is never free. Its description sits in context every turn just so Claude knows it exists. And there's a ceiling: the whole skill listing gets a budget, default 1% of the context window. Blow it and Claude Code drops full descriptions for your least-used skills to make the rest fit. A skill with no description can't trigger anything, it's effectively gone while looking installed. People with big kits are seeing "122 descriptions dropped" when they check /doctor, and since the warning moved out of startup, most of them found out late. Path rules cost nothing until a matching file is touched and there's no budget to blow.

Certainty. A skill has to be picked by the model, and with 100 descriptions to scan, sometimes it isn't. A path rule involves no decision. Claude touches the file, the rule is there. Guaranteed. For the stuff Claude consistently gets wrong, you do not want a drifting model choosing whether to load its own correction.

If your kit has a pile of "knowledge" skills, you have rules wearing the wrong costume.

Known issues, rules:

  • Path rules fire on READING a matching file, not always when creating a brand new one. Keep creation-time rules in CLAUDE.md.
  • Path rules in ~/.claude/rules/ have been silently ignored. Keep them at project level.
  • No @ imports in rules files, unlike CLAUDE.md. Symlink to share.
  • Loading failures are silent. Run /memory and check what actually loaded, never assume.

SKILLS. Procedures you invoke: deploy, scaffold, cut a release. Only the description sits in context, the body loads when it fires. The full current frontmatter, you'd normally use three or four of these:

---
name: deploy                    # display label only. The FOLDER name is what you type after /.
                                #   Safest is to omit it, see known issues
description: Deploy the current branch. Use when the user asks to deploy, ship, release.
when_to_use: |                  # extra trigger phrases, appended to description.
  - "deploy", "ship it", "push to prod"
  - do NOT use for staging
arguments: [environment]        # named args, use $environment instead of $ARGUMENTS[0]
disable-model-invocation: true  # only YOU can run it. Set on anything with side effects
user-invocable: true            # false hides it from the / menu, background knowledge only
allowed-tools: Bash(git *)      # pre-approved while active, kills permission prompts
disallowed-tools: AskUserQuestion  # removed from the pool while active
model: inherit                  # or pin one, just while this skill runs
effort: medium                  # reasoning effort while active
context: fork                   # run in a throwaway subagent instead of your thread
agent: general-purpose          # which agent type runs it when forked
paths: ["src/**"]               # auto-load near matching files, like a rule
shell: bash                     # or powershell, for !`command` blocks in the body
---

The two fields that fix "my skill never fires": description IS the trigger, write it like you're instructing someone else's AI. And when_to_use, which I almost never see in the wild even though it exists exactly for this. Both sit in context every turn though, every trigger phrase you add grows the always-loaded footprint. One more reason knowledge belongs in path rules.

Known issues, skills:

  • The name field is a trap. The dropdown shows the frontmatter name, but tab-complete inserts the FOLDER name into your prompt. If they differ, what you see is not what you send. Found this one myself. On top of that, the VS Code extension silently refuses to load a skill whose name doesn't match its folder. Simplest fix: never set name, let it default to the folder.
  • Skills with paths: frontmatter have been reported completely undiscoverable, gone from autocomplete, "Unknown skill" on direct invoke, until the paths line is removed. Test yours before trusting it.
  • The skill listing is budgeted at 1% of the context window by default. Past that, your LEAST-USED skills lose their descriptions entirely, and a skill without a description never fires. Run /skills to cull, or raise skillListingBudgetFraction in settings.json if you accept the token cost.
  • Long sessions cull skills too. After a compact, invoked skill bodies get re-injected, but the oldest ones drop once the re-injection budget is exceeded. A skill you used early in a session may simply not be there anymore.
  • Malformed YAML frontmatter does not fail loudly. The skill loads with EMPTY metadata, your description and triggers silently vanish, and the skill never fires while looking installed. One bad colon is enough. Check /context.
  • user-invocable defaults to true, so every skill lands in the / menu. Pure knowledge skills need an explicit false or they clutter it.
  • disable-model-invocation also drops the skill from Claude's context entirely. Claude can't even see it exists, which is the point, but surprises people.
  • Once a skill fires, its body stays in context for the rest of the session. Write it as standing guidance and keep it tight.

AGENTS. A separate Claude with its own context window. Never sees your conversation, hands back a summary. For review, heavy reading, and parallel jobs. Roughly 7x the tokens, you pay for isolation, not savings.

---
name: security-reviewer         # required. How you @-mention it
description: Use this agent when reviewing code for security issues. Use proactively after auth changes.
tools: Read, Grep, Glob         # ALLOWLIST. Omit and it inherits everything. Scope it
disallowedTools: Write, Edit    # denylist, subtracted from the rest
model: inherit                  # or pin haiku for cheap jobs, opus for hard ones
permissionMode: default         # or plan for read-only exploration
maxTurns: 20                    # hard cap, keeps a runaway agent bounded
skills: [security-patterns]     # preloaded at startup, full content injected
mcpServers: [github]            # define one inline HERE to keep its tools out of your main context
memory: project                 # persistent notes across sessions. user, project, or local
background: false               # unset = Claude decides, it backgrounds by default now
effort: high
isolation: worktree             # runs in a temp git worktree, changes stay off your checkout
---
You are a senior security reviewer. Report findings by severity with file and line. Do not modify code.

Known issues, agents:

  • Agents loaded from a plugin ignore hooks, mcpServers, and permissionMode for security. If you need those, keep the agent in .claude/agents/, not a plugin.
  • Omitting tools means it inherits EVERYTHING, including Write. Scope your reviewers.

HOOKS. The only layer that enforces. No frontmatter, it's wiring in settings.json plus a script. There are 30 documented events right now. The full list, grouped by when they fire:

Session:    SessionStart, SessionEnd, Setup
Per turn:   UserPromptSubmit, UserPromptExpansion, Stop, StopFailure
Per tool:   PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch,
            PermissionRequest, PermissionDenied
Subagents:  SubagentStart, SubagentStop, TeammateIdle
Context:    PreCompact, PostCompact, InstructionsLoaded, ConfigChange, CwdChanged
Files/misc: FileChanged, WorktreeCreate, WorktreeRemove, TaskCreated, TaskCompleted,
            Notification, MessageDisplay, Elicitation, ElicitationResult

You will build almost everything from five of them:

PreToolUse, before a tool runs. The only real gate. Blocks with exit 2, can even rewrite the tool's input. Its deny beats bypassPermissions and --dangerously-skip-permissions, and a hook can only tighten, never loosen. PostToolUse, after a tool succeeds. Format, lint, test, log. Cannot undo, the tool already ran. UserPromptSubmit, before Claude sees your prompt. Inject context (stdout becomes context) or reject the prompt. SessionStart, on start or resume. Inject branch, dirty state, whatever you want up front. Stop, when Claude finishes a response. Exit 2 here means "keep working". It fires on EVERY response end, not just task completion, so gate it on a real check or you build an infinite loop.

The wiring and the script contract:

// settings.json
"hooks": {
  "PreToolUse": [{
    "matcher": "Bash",            // which tool. Regex ok. Omit = everything
    "hooks": [{
      "type": "command",          // or http, or a prompt (LLM check)
      "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh",
      "timeout": 10,
      "async": false              // true = fire-and-forget, CANNOT block
    }]
  }]
}

#!/bin/bash
input=$(cat)   # event JSON on stdin
# exit 0 = allow. exit 2 = BLOCK, stderr goes back to Claude as the reason.
# exit 1 = non-blocking error, the action RUNS ANYWAY. The footgun.
exit 2

Known issues, hooks:

  • Exit 2 blocks. Exit 1 does not, the action runs anyway. This one detail kills half the security hooks people write.
  • And exit 2 only blocks on events that CAN block. On PostToolUse it blocks nothing, the tool already ran. Prevention is PreToolUse only.
  • async: true cannot block, rewrite, or inject context. Exit 2 in an async hook does nothing.
  • A hook never fires on @-referenced files, no tool call means no hook. Protect secret paths with a Read deny in settings.json permissions instead. Deny beats everything.
  • Don't use an MCP tool as a security hook handler. If the server disconnects, the hook degrades to a non-blocking error and the action proceeds. Hard policy is a command hook.
  • Matcher is case-sensitive. Edit|Write, not edit|write. And a script without chmod +x fails silently.

CLAUDE.md, for completeness: no frontmatter at all. Plain markdown, loads every turn, all of it, always. Most expensive real estate you own. Under 200 lines or adherence drops. And it is a suggestion, not law.

Before someone asks: yes, a CLAUDE.md inside a subfolder is the other lazy loader. It only loads when Claude first touches a file in that folder, which sounds like a path rule. Two reasons the rule still wins. A subdirectory CLAUDE.md does NOT survive a compact, root re-injects itself, the nested one silently drops until you touch the folder again, so mid-session your rules can just be gone. And it's one file per folder, while a path rule scopes by glob across the whole repo, "**/*.repository.ts" doesn't live in any one folder. Also the name must be caps, a lowercase claude.md silently does not load.

The whole guide compresses to one sorting test. Must it hold no matter what? Hook. Knowledge tied to certain files? Path rule. A procedure you invoke? Skill. Needs its own context? Agent. True everywhere, all the time? CLAUDE.md, and keep it short. Skills help, hooks enforce.

So, what's wrong, what's missing, what issue did I not list? You found the holes last time. Do it again.

reddit.com
u/TimAtMongoDB — 19 days ago
▲ 20 r/ContextEngineering+1 crossposts

AMA with MongoDB: Max Marcon (Director of Product), Mikiko Bazeley (Staff Developer Advocate), and Yang Li (Senior Solutions Architect). They work on AI agents in production. Ask them anything about context engineering at our AMA next Wednesday (7/8)!

Hi r/ContextEngineering!

I’m Nina (u/ContextualNina), your friendly AMA moderator for next week, the inaugural AMA for this subreddit! I’m excited to introduce the three people who will be taking all of your questions for our upcoming AMA: Max Marcon (u/mmarcon), Mikiko Bazeley (u/mmbaze), and Yang Li (u/Ok-Amphibian6116). Between the three of them, they spend a lot of time working with teams building AI agent systems that need to hold up in production.

Ask them anything during a live AMA right here on Wednesday, July 8 from 12-1 PM ET (9-10 AM PT). The real tradeoffs, the messy parts, AI hype vs. reality - whatever you’ve got.

I invited this group because they work directly on the data layer for production AI agents, which gives them a pretty grounded view of where things get hard: context design, retrieval quality, memory, state, multi-step workflows, and the parts of agent systems that tend to fail outside of demos.

We’ll be answering questions about:

  • Where context engineering ends and memory engineering begins
  • What “context rot” looks like as context gets longer
  • How to think about memory in multi-agent systems
  • When RAG beats long context, and when it doesn’t
  • The context mistakes that can quietly sink agent systems in production

You can start dropping in questions now ahead of time (they’ll answer them during the live window), or ask them live next Wednesday!

Full disclosure: I’m the founding mod of this subreddit, and I recently started at MongoDB. I thought this subreddit could benefit from chatting with some of my new colleagues.

https://preview.redd.it/cclnm62oqoah1.jpg?width=720&format=pjpg&auto=webp&s=29a99450aedd525142f33da5dfef545874c8715a

https://preview.redd.it/dlgzx0dpqoah1.jpg?width=720&format=pjpg&auto=webp&s=695190cb7e5bce175ea56ab7726899f1dd6a1d7b

https://preview.redd.it/tecqw15qqoah1.jpg?width=1440&format=pjpg&auto=webp&s=256dcfaa205a8e94161dfdb3fb5997784ad7d196

reddit.com
u/ContextualNina — 2 months ago

Why are all the Claude Code skill files I see online completely pointless?

Every skill file I come across looks like this:

“You are an expert full-stack developer with 20 years of experience in React, Node.js, and TypeScript. Always write clean, maintainable code.”

Claude already knows all of this. You’re not teaching it anything.

The whole point of a skill is to fix something Claude consistently gets wrong. Not to explain what a developer is.

And Claude gets a lot wrong. Stuff a real developer would never skip:

•	Performance is never considered upfront. No thought for render-blocking resources, what to inline, what to defer. You find out at Lighthouse time.    
•	Mobile layout is an afterthought. A real developer thinks responsive from line one.    
•	Nobody ever mentions CSP or a WAF before deploying something public. A senior dev would bring it up unprompted.    
•	Accessibility gets skipped entirely. Clickable divs instead of buttons, no focus management, ARIA slapped on at the end if at all.

These are the things skills should be fixing. Not reminding Claude that it’s a “world class engineer.”

Am I missing something? Are there actually good skill files out there? Because I can’t find them.

reddit.com
u/TimAtMongoDB — 2 months ago
▲ 895 r/Agent_AI+1 crossposts

Why are all the Claude Code skill files I see online completely pointless?

Every skill file I come across looks like this:

“You are an expert full-stack developer with 20 years of experience in React, Node.js, and TypeScript. Always write clean, maintainable code.”

Claude already knows all of this. You’re not teaching it anything.

The whole point of a skill is to fix something Claude consistently gets wrong. Not to explain what a developer is.

And Claude gets a lot wrong. Stuff a real developer would never skip:

•	Performance is never considered upfront. No thought for render-blocking resources, what to inline, what to defer. You find out at Lighthouse time.  
•	Mobile layout is an afterthought. A real developer thinks responsive from line one.  
•	Nobody ever mentions CSP or a WAF before deploying something public. A senior dev would bring it up unprompted.  
•	Accessibility gets skipped entirely. Clickable divs instead of buttons, no focus management, ARIA slapped on at the end if at all.

These are the things skills should be fixing. Not reminding Claude that it’s a “world class engineer.”

Am I missing something? Are there actually good skill files out there? Because I can’t find them.

reddit.com
u/Money-Ranger-6520 — 2 months ago
▲ 36 r/mongodb

Just joined the MongoDB team, happy to help where I can

Hey r/mongodb. I just started as a Staff Content Engineer at MongoDB and wanted to introduce myself.

I've been building production systems with MongoDB for years so I'm not new to the actual problems that come up here.

If you're stuck on aggregation pipelines, data modeling, driver questions, or anything else, drop it in the comments or DM me.

No corporate agenda, just here to be useful.

reddit.com
u/TimAtMongoDB — 3 months ago