These 90 Minutes Will Change the Way You Use AI

Timestamps

  • - Introduction
  • - Where Would You Use an LLM?
  • - Instruction Files
  • - Multi Tenant Service
  • - Convert Thinking into Deterministic Rules
  • - Using Checks to Enforce Quality
  • - Linting
  • - Components
  • - Self Learning AI
  • - Skills and Subagents
  • - Main Orchestrator
  • - APIs and MCP
  • - AI Features vs MCP
  • - Permissions
  • - Isolated LLMs
  • - Identify Workflow Problems
  • - Prototyping with AI
  • - Hooks
  • - Debugging and Listeners
  • - Monitors
  • - Approach to Building with LLMs
  • - Next Steps

This video is a walkthrough of how I actually use AI day to day running my course platform. Not prompting tips, the system around the AI: instruction files, checks, skills, subagents, hooks, and permissions. The notes below are grouped to match the timestamps at the top, so you can jump around.

Where Would You Use an LLM?

The magic of an LLM is that it can make decisions, which is something that's hard to articulate in a script without covering every possible scenario. The tradeoff is the output is unknown. Ask it the same question twice and you might get two different answers.

This is called non-deterministic: same input, potentially different output. That's actually one of the strengths of an LLM, if you use it in the right place.

The mistake a lot of people make is using something non-deterministic to do work that's deterministic (same input always gives the same output). If the task is deterministic, you want a script, it'll give you the same result every single time. This framing carries through the whole lesson.

Instruction Files

Every AI coding tool supports some kind of instruction file (Claude Code uses CLAUDE.md). This is just a markdown file in your project that gets loaded in to every conversation.

The catch is that the AI interprets the file. It's great for instruction, but it's not ideal for enforcing things. That was the biggest breakthrough I had: I was instructing the LLM through a bunch of rules written in md files, and they were often not being followed, or I was getting inconsistent results.

Still treat it as a living document. Any time the AI does something you didn't want, ask whether a rule would have prevented it, then add the rule:

## Database
- Never connect to the database directly, use the helper commands
- Never push schema changes, always require a migration file

## UI
- Check if a component exists before creating a new one
- No inline styles except for dynamic values

Mine has grown to cover naming conventions, styling, imports, and a checklist the AI runs through before writing code. Just don't expect any of it to be followed 100% of the time. Fixing that expectation is what the next few sections are about.

Multi Tenant Service

A concrete example of the problem. My project is a multi-tenant service: one database serving multiple instances, and each instance serves its own end users. Data should never cross between tenants, ever.

My first attempt was an instruction, basically "respect tenant scoping". That's risky, because the moment it gets skipped you're showing somebody data that belongs to someone else.

So instead of relying on the rule, we extended the database layer to always infuse the tenant. A query like SELECT * FROM products never reaches the database without the tenant attached, the adapter adds it automatically. Now it can't be forgotten, because there's nothing to remember. This is exactly the kind of thing that gets missed when someone is vibe coding with no insight into how things actually work.

Convert Thinking into Deterministic Rules

An LLM is non-deterministic. A script is not. Whenever the AI keeps making the same judgment call, I try to turn that judgment in to a check that runs the same way every time. I call these gates: the result has to pass them to count as done. Lint rules are the easiest example:

JavaScript
// ESLint: ban reading env vars directly, force the validated module
"no-restricted-syntax": [
  "error",
  {
    "selector": "MemberExpression[object.property.name='env']",
    "message": "Read env vars through the validated env module."
  }
]

Once a rule is a lint error, you never have to remind the AI again. It hits the error, reads the message, and fixes itself. The message IS the documentation.

You don't have to write these yourself either. Ask your AI to look at your code and figure out what could be converted into checks that always run, you'd be surprised what it comes up with.

There's a second benefit: tokens. Running a script is cheap, asking an AI to scan your project for issues is expensive. Fine to do every now and again, but if you want it on every code change, make it a script.

For the stuff that's too fuzzy for a script, pair the gates with a verifier, a subagent with a rules file of opinionated checks: code smells, anti-patterns, standards you've defined. Removing everything deterministic from the verifier keeps it focused. A classic entry in that rules file is N+1 queries, retrieving a set of elements and then hitting the database once per element in a loop. Hard to lint for, easy for a verifier to spot.

Using Checks to Enforce Quality

The goal here is a little counterintuitive: we're making it more difficult for code to reach production, not easier. Everybody wants AI so code ships faster, but the checks are what let you ship fast without shipping garbage. The layers stack:

  • Your stack choices (I use TypeScript over JavaScript, with no implicit or explicit any)
  • CLAUDE.md instructions
  • Linting and custom check scripts
  • A verifier subagent for the opinionated stuff
  • Hooks, in the AI or in git
  • Automated tests
  • AI code review on pull requests, tied into CI/CD

Do every one of these and your software will be dramatically better than someone who opens a terminal and says "yo build me something cool, don't make any mistakes".

Linting

A few lint setups that pay off for pretty much any project.

First, ratcheting. Say you have a thousand TypeScript errors. Set it up so the count can only ever go down: new errors block progress like a compilation error, existing ones are grandfathered in. It's a way to acknowledge the bad past and set new standards going forward. Ask your LLM to set this up, it can figure it out.

The one that made the biggest difference for me is a line count limit on files. Mine is set at 1000, adjust for how you split your files. I was working on a builder that was a few thousand lines, and any time the AI changed anything it had some side effect breaking something else. Every change introduced a regression, or it would bring back earlier features, or just make stuff up. That problem is way less common with smaller files. I think AI performs best in the 500 to 1000 line range. I still have around 20 files breaking my own rule, which is why I ratchet it instead of failing outright.

Related: extract logic into pure, testable functions. The component keeps the visual layer and the styling, the logic lives in functions the component calls. Those functions get basic unit tests. If you don't want to build big end-to-end suites with Playwright, this is the low-hanging fruit.

This matters most when you generalize components. Say a page uses some component and later you want the same thing somewhere else. Copying it means variations to manage, and any core change has to happen in multiple files that drift out of sync. Generalizing it means abstraction and conditionals, and what worked 100% of the time now works 95%. I hit this with a checkout modal, a sensitive part of the app I did not want to touch once it finally worked, so I duplicated it and paid for that for a long time. Tests on the component are what make extraction safe later.

Some other custom checks I run: synchronization checks so my API layer and its documentation can't drift (the AI wrote these itself when I asked if drift was preventable), every endpoint required to have authentication and validation, no server imports in client code, and an environment variable check so a deploy can't go out missing a var. That last one has actually saved me, I made some small change, went to deploy, and the check said no, your environment variables don't match. Plus the standard stuff: strict TypeScript, Prettier formatting enforced. Pretty much anything pre-AI you'd enforce at compile time or in CI, do all of it.

Components

For instructions the AI needs on demand, one massive CLAUDE.md dilutes everything with junk. What works better is using CLAUDE.md as a table of contents pointing at scoped files.

I keep those files in a vault/ folder at the root of the repo and work with them through Obsidian. I started on Notion, but Obsidian gave me the same experience with raw md files that live in the repo, so the AI can read them directly.

The first file I'd create is components.md, a catalog of every reusable component: the selects, the paginated table, whatever you have. Then a rule: check components.md before creating new components. Without this the AI will happily build you a fourth variation of the same button, I had that exact situation on one page.

At one point my CLAUDE.md said "do not use alerts" multiple times and it would just keep using alerts, completely ignoring me. The fix was moving it into a lint rule. No alerts, problem solved. Same with console logs. If it's not very important, CLAUDE.md is fine. If it's important, tie it into linting somehow. You could even lint against raw HTML elements so everything has to go through your component directory. I haven't personally done that one, but it would work.

Self Learning AI

The next level of the vault: structure it by the domains of your app (services, security, auth, the big features) and tell the AI in CLAUDE.md to update these documents as it goes. Feature specs, architecture decisions, migrations in progress, anything that's a big project gets documented as it happens.

Honestly these notes are less for me and more for future conversations with the LLM. It can refer back to decisions instead of relying on memory.

Here's why I think this matters. With AI, single developers are building full software suites, and the limitation is no longer the ability to code. Zoomed out it's the ability to build something people want. Zoomed in, it's remembering everything you decided: the architecture, the design documents, the choices. That's what the vault holds.

And when the AI makes a mistake and you correct it, that correction should land somewhere permanent: the instruction file, a lint rule, or the vault. The workflow slowly gets better because every failure becomes a rule.

Skills and Subagents

Two tools in Claude Code that sound similar but aren't. The difference is where the context goes: a skill brings all of its information into your main chat's context, a subagent manages its own context and only sees what you hand it.

So the split is: anything you repeat and want available mid-conversation goes in a skill (design guidelines, content planning, making a PR). Anything you can isolate goes in a subagent (research that summarizes something from the internet, a test writer that works on the side while you build). The value of the subagent is it does all its work in its own little bubble and just hands back the result, so your main context stays clean.

Main Orchestrator

For more complex work I take this further: the main chat becomes an orchestrator that launches bundles in the background. Each bundle has its own sub-orchestrator in charge of building one feature end to end, and it delegates: a planner writes the spec, an implementer codes it, a tester writes tests and makes sure existing ones still pass, and the verifier/challenger checks that what we built is what we wanted and isn't overcomplicated. You can think of it as building a small team for a goal.

I wrote all of this up as a skill, I called it launch. I describe the feature, it launches a bundle in the background, and the main loop stays free so I can keep chatting or launch more bundles in parallel. I don't have to wait an hour or two for one to finish.

A few things that make it work:

  • The spec is the authority. Most questions happen up front while designing it, and the other agents just implement, test, and verify against it. Front-loading the design questions is what lets it run for hours without me.
  • Agents triage their questions and only bubble up the ones above a threshold that actually affect design or implementation choices.
  • You can assign different models to different roles: a heavier thinking model for the planner and manager, cheaper models for implementation that's just following the spec.

Fair warning: done poorly this just adds bureaucracy to your AI agents, they go back and forth changing each other's work and burn credits without converging. I'd only recommend this structure once everything earlier in this lesson is already working for you, and probably only on a plan where you have credits to spare. It's overkill for small stuff.

APIs and MCP

Build a real API first, then expose it through MCP. MCP is a standard for giving an LLM access to tools, you can think of it as an API designed for agents. Once my platform had a public API, wrapping it in an MCP server meant you can connect your AI and say "draft up a high ticket product, create some landing pages, draft some emails" and it all happens on your live account. The API is the product, MCP is the adapter.

When a service you use has no MCP server, check for an API. An LLM can do pretty much the same stuff with an API key and the documentation, especially if the API has an OpenAPI spec, mine does. Worst case you can grab your cookies from dev tools and let the AI act through the browser session, but that's the high-risk option. Check for MCP first, then API, then get creative.

AI Features vs MCP

Put your developer hat on for this one. You can build AI features into your app, buttons that summarize documents or generate things. Or you can expose those same capabilities through MCP and let the user's own LLM do the work. There's a lot of overlap.

I had started building page generation and editing inside my app, and I paused it and moved that capability into the MCP instead. Two reasons. One entry point for doing anything in the app made more sense than sprinkling AI features everywhere. And with in-app features I pay the tokens and have to manage limits and credits, with MCP the user's LLM pays. I just didn't want to start with all that complexity.

It doesn't always go that way, though. My users' end users are a different story: someone watching a course video who wants to ask a question about the lesson is not going to connect an LLM first, they just want to click a button. For that audience the AI belongs inside the app. Think about who the user is and what they'll actually tolerate setting up.

One layer deeper: an MCP server is a collection of tools, and I mapped mine one-to-one with my API endpoints, which made the MCP layer very quick to build. Each tool carries a description, which is context you feed the LLM so it knows how to use the tool correctly without guessing. That's the main edge MCP has over handing it a raw API key. I might build more purpose-driven tools eventually, but one-to-one got it shipped.

Permissions

Claude Code has a flag to skip all permission prompts. I avoided it for the longest time, I assumed it was just reckless. What I figured out is it's not the command's problem, it's the user's problem if they use it without the right setup. Before you go anywhere near it, do the boring version first: add your common commands to the allow and deny lists. Those persist between sessions, unlike the yes buttons you click mid-session, which only last for that session.

The deny list is where it gets interesting. I denied direct psql and required going through docker exec instead, so the AI can reach the dockerized project database but can't guess its way into some other database on my system (I had a locally installed Postgres it kept trying to use). I deny db push style commands that skip migrations, because applying directly to the database creates drift between the schema and the migration files. Anything destructive in version control, like git reset, is worth denying too.

One mental adjustment: think of these permissions as guidance for the LLM, not security. They genuinely help it pick the right database and avoid destructive habits, and after you deny something once it stops for every future session. But there are ways to mask the true intent of a command, so if you imagine a genuinely bad AI or a prompt injection attack, deny rules alone won't save you. That's what isolation is for.

Isolated LLMs

If your machine has the local connection string and the production connection string sitting next to each other, and your plan is telling the LLM to choose carefully, you're set up for a bad day. We don't want to instruct the AI to be careful, we want to make the bad thing impossible by removing access altogether.

So I run Claude inside a Docker container. The container mounts the repo and nothing else. My GitHub credentials, my cloud provider CLIs with keys to real infrastructure, anything sensitive lives outside the repo, so the container simply cannot touch it. It can make commits locally, but it can't push or pull from my private repo, because the credentials aren't there.

The split that falls out of this: long-running autonomous work happens inside the container with permissions relaxed. Anything that touches GitHub or production runs from Claude outside the container with permissions ON, so I see every command that could hurt something. I run multiple containers at once, all working in the same repo, sharing a database container for the app but each with its own test database so they can run tests independently.

Identify Workflow Problems

A small instruction with outsized returns: tell the AI, in CLAUDE.md, to identify workflow problems and improve them. Specifically the skills and subagents you've defined and your document structures. It's not guaranteed to run every moment, but having it in context means the setup improves as you use it.

And whenever I notice the AI doing something funky, I just tell it to take the issue we hit and put it in the vault so we don't run in to it again. The goal is improvement happening recurrently instead of me manually tuning things forever.

Prototyping with AI

I'm by no means a design expert, and this is what got me furthest: anchor every request to an example. I spent some time up front having the AI build a reference library in my vault, example themes, landing pages, widgets, analytics views, about a hundred pages as one big HTML file I can click through. None of it ships. When I need to design something, I find a page I like and say "design it based off this example". Anchored instructions get much better results than descriptions.

The other half is prototyping flows before wiring anything up. Payment flows are painful to test for real: create an account, buy the product, then revoke it to test again. Instead I had the AI build the whole thing as front end only, with buttons to flip between states: not logged in, logged in and owns the product, logged in and hasn't bought. I click through every path, fix what looks wrong, and then that prototype becomes the spec for the real implementation. By the time the backend exists there's barely anything left to discover. Raw HTML and CSS is usually enough, sometimes React if I want it more engaging.

Hooks

Hooks are code that runs at fixed points in the agent's loop, before a tool call, after a file edit, when a session ends. Very similar to middleware in web requests. Unlike instructions, a hook can't be ignored or forgotten, it executes every time:

JSON
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "npm run lint:changed" }]
      }
    ]
  }
}

There are a bunch of events (SessionStart, UserPromptSubmit, PreToolUse, SubagentStart, Stop, and more), the full list with input schemas is in the hooks documentation. The two I lean on: PreToolUse with a matcher to intercept and block dangerous executions before they run, and Stop, which fires after the AI finishes a full response.

A hook is a script, not a prompt. The script runs, and its return gets fed back to the LLM, which can send it back to fix its work. Mine are just JavaScript files: the PreToolUse one blocks bad commands, the Stop one runs the lint ratchet so a response can't end with new violations. Worth asking for each check: does this need to run after every single tool use, or can it wait until the end of the response? I moved most of mine to Stop.

One more use case: people run with full permissions but use hooks to block specific things, since bypassing permissions ignores allow/deny rules but hooks still fire.

Debugging and Listeners

Tests first, as always, and pure functions make the tests worth something: isolate logic with no side effects, and after a change, same input giving the same output means high confidence nothing broke. I once added a new coupon type tied to accounts and had to re-verify every place a coupon gets used on the site. Smoke testing a large app by hand is exhausting, coverage is what makes changes like that cheap.

Two things AI is unusually good at here. First, tracing: when a request returns something you don't expect, don't hand the AI the error, ask it to trace the request end to end from request to response. It's much better than us at jumping between functions, and it'll find the transformation you weren't expecting. It can run its own servers and read its own logs along the way.

Second, reading logs. The core of what these models do is find associations in inputs too high-dimensional for a human, and log analysis is exactly that shape. Get production logs in front of the LLM and it can sift for patterns, connect requests, spot the weird thing. I wrote a skill instructing how I want logs read and now I can pass it a request and ask arbitrary questions, way easier than digging through the logs myself.

Monitors

Monitors take the log idea real time: an event happens on production, the log gets forwarded to your LLM, and it reasons about whether it's a problem. This is a layer above normal alerting and observability, because it's not triggering on a threshold or a specific error, it can think through whether something strange is happening across the logs.

This is how you get autonomous AI that isn't just building your software but actually running your operations. I feel like I've got the building side locked in, so this is where my time is going next.

Approach to Building with LLMs

You'll progress through stages: AI as an assistant, then it writes code and you go back and forth, then it writes big sections you review, then multiple agents running at once. These days I'll literally have four Claude windows on my desktop doing different things, or several bundles running inside one instance. Since a feature takes an hour or a few to build and verify on its own, running four at once is not as crazy as it sounds.

Some habits that make the parallel thing work:

  • Keep conversations focused and clear the context when you switch topics.
  • Commit freely. I used pre-commit hooks for a while and they just made me delay commits because every commit meant fixing everything first. Now I don't care what's committed, I only care what's pushed to production, that's a separate step in my mind.
  • Worktrees give agents isolated copies of the code, and they're the common answer for multiple LLMs on one codebase. I haven't needed them, I just scope the parallel work to different areas of the app (Slack notifications here, MCP layer there, design work in the third) and told the AI in the instructions that other agents may be editing files, so respect changes it didn't make.

On reviewing everything: I don't. The more important the code, the more attention it gets. My page builder renders from a JSON structure where old versions have to keep rendering, so changes there are low stakes and I barely read them, same with design work, I check that it looks right and the structure is sane. Payments code I read closely. Right now I work as an operator managing these different agents and reviewing what matters before anything deploys.

I still have days where I turn the AI off and code by hand, and it's usually the right call for anything I don't understand yet. I needed a feature that summarized notes into a chat system with vectorized data in the database, all new territory for me, and having the AI just implement it produced code I didn't understand. I took a couple of days to build the core myself, and then having the AI extend it was easy, because I could actually review the changes.

Last one: I have a separate Claude review PRs going into main, and it finds real issues precisely because it isn't the instance that wrote the code. Cleared context, reading with the intent of finding problems. Same spirit as the verifier, just tied into GitHub.

The bigger picture: before AI, a software org wasn't all people writing code. The higher you went, the more you thought about bigger problems, reviewed code, and drove features instead of implementing. That's the mode AI unlocks for a single developer, and if you don't develop that skill you're going to struggle.