Crystallize logo

How We Gave an AI Agent Our Codebase Without Giving It the Keys?

Inside the internal agent Crystallize engineers work with every day, and the security model that keeps your data and our code where they belong.

clockPublished October 13, 2026
clock13 minutes
Sébastien Morel
Sébastien Morel
How We Gave an AI Agent Our Codebase Without Giving It the Keys?

If you follow Crystallize, you have seen a lot of AI from us lately: our MCP server, Claude skills, hyper-personalization and bookable commerce features, amazing livestreams, and the methodology we use to start new AI projects from day one. Not to mention Superfast.shop, a catalog of production-shaped commerce systems built with AI and Crystallize.

We do just as much on the inside.

This post is about Claudette, the AI engineer who lives in our Slack, reads our private codebase, investigates production problems, and prepares code changes—without access to the credentials that could put your data or our intellectual property at risk.

What Claudette Does

Claudette is an internal assistant for our teams. They use it to:

  • Answer technical questions in Slack, grounded in the actual code rather than memory or outdated notes.
  • Act as a private co-pilot while an engineer handles a support case: explain the cause, suggest the next check, and draft a reply.
  • Turn a conversation that lands on a fix into a written plan in our project management system.
  • Implement a plan as draft pull requests when an engineer asks for it.
  • Produce an on-demand health report from our hosting, database, and incident monitoring.
  • Check our public developer documentation against the code and prepare corrections as drafts directly in Crystallize, our documentation platform.

How It Is Put Together

"Agent" can mean several things, and for security, the difference matters, so here is what runs where.

Claudette itself is an orchestrator. It is our application, running on our own host. It receives requests from Slack, checks who is asking, decides which model and which tools fit the topic, and holds the credentials it needs to reach our systems, exactly like any web application with its configuration file.

For each request, the orchestrator launches a Claude Code session. Claude Code, through Anthropic's Claude Agent SDK, is where the model reasons, reads code, and calls tools. Investigations and code changes go to our strongest model; routine readings go to a faster, lighter one.

The application's credentials stay with the orchestrator. When the model needs something from our infrastructure, it calls a tool; the orchestrator runs the request with its own credentials and returns only the answer. Everything the model can touch directly- its shell commands and file reads- sits behind a boundary where no credential can be read.

The orchestrator holds the application's credentials, like any web application. The Claude Code session it launches gets only what it needs to reach the model and the incident system, and nothing the model can call can read a credential.

First, What this Means for Your Data

When we talk about AI agents with customers, the first question is always about data. So, plainly:

  • Claudette is internal. Only Crystallize employees can use it, and we check every request against our company directory before anything runs.
  • It cannot change anything in our infrastructure. Every connection to hosting, the database, and incident monitoring is read-only.
  • It sees no more than a Crystallize engineer. It works from our source code and from operational signals such as metrics, logs, and incidents, the same tools our team uses to keep the platform healthy. It doesn't log into your tenant or use our APIs on your behalf, and on the database side, it only has monitoring access.
  • The model can never read credentials. The model can't read any password, API key, or token through its shell, file tools, or anything else it can inspect. This is the part of the design we care about most, and it is explained below.
  • Nothing goes live without a person. Claudette cannot merge code or publish a page. An engineer reviews and does that.
  • Nothing is shared beyond the model. What Claudette reads is processed through Anthropic's API, and it has no integration that uploads or shares data anywhere else.

Our Code Stays Ours

Claudette reads our private repositories, so it must protect them, too. Each repository is tagged public or internal, and that tag decides what Claudette may quote back:

  • In a support channel, it explains how things behave but never reproduces internal source code, because answers there are often shared with customers.
  • In a private conversation with one engineer, it may show that engineer internal code, but it first sanitizes anything it drafts for a customer.
  • When it writes code or documentation that will become public, nothing from an internal repository may appear in it.
  • Secrets (environment variables or credentials) are never shown to anyone, in any situation.

Security by Construction

A model with a shell and access to monitoring is only acceptable if "read-only" is enforced by something stronger than an instruction in a prompt. Models can be wrong, and they can be manipulated by text they read. So for every capability we ask one question: what still holds if the model is fooled? The answer must be enforced somewhere the model cannot reach.

Every Command Runs in a Sandbox

The model can run shell commands to search and analyze code, but every command runs inside an operating-system sandbox: Seatbelt on macOS, bubblewrap on Linux. Outside a scratch directory, the filesystem is read-only. Configuration files, credentials, stored conversations, and internal databases are unreadable. If a command cannot be sandboxed, it does not run. There is no fallback.

Our first version filtered commands against a list of safe tools. We removed it after seeing how easily one shell line can hide a second command. A text filter is not a security boundary; the operating system is.

The model's file-reading tools run outside the sandbox, so the same list of forbidden paths is applied to them as permission rules. One function produces that list for both, so the sandbox and the permission rules cannot drift apart.

Secrets the Model Can Never Read

Claudette needs credentials to do its job: to pull our repositories, query monitoring, save plans, open pull requests, and save documentation drafts. They live in the orchestrator, and none of them is readable from where the model works.

  • The Claude Code session is started with an environment built for it, not a copy of the orchestrator's, and credentials are removed again from every shell command the model runs.
  • The repository token is never written into a clone URL or a git configuration file, where the model could read it. The orchestrator attaches it to each git request as a header at the moment of the call, and never as a command-line argument that would show up in the process list.
  • Reading and writing use two separate tokens. The read token never gains write access, and the write token used for pull requests belongs to a dedicated bot account with only the permissions it needs. It never touches a copy of the code the model can read.
  • The orchestrator runs tools for hosting, database monitoring, plans, pull requests, and documentation. The model asks a question, the orchestrator runs it with its credentials, and only the answer comes back.
  • The incident system is the one connection Claude Code makes directly, so its read-only token is handed to the Claude Code process. It never enters the sandbox, and only an allowlist of read tools can be called with it.

The principle underneath is simple: a model cannot leak a secret it cannot read. So we design the system so it cannot read one.

Read-only, Enforced at the Source

Each monitoring connection is read-only where it counts, at the provider, and we chose the integration for each one by how precisely its permissions can be limited:

  • Hosting: the official MCP server, plus a tool of our own. We run the cloud provider's official MCP server in read-only mode for documentation and region information. In that mode, it offers no tool to read our actual resources, so we wrote a single tool that runs the cloud CLI inside the orchestrator with a read-only profile. It takes structured parameters, has no shell, and refuses anything that isn't a read or any operation that would return a credential.
  • Database: our own tools. The vendor's MCP server didn't provide a fine-grained enough permission model, so we built our own monitoring tools. Our client can only send read requests, and the account only holds monitoring roles: cluster health, metrics, performance advice, and alerts, never the data itself.
  • Incident system: the vendor's MCP server, locked down. Here the hosted server fits, but the provider cannot tell us what a token is allowed to do. So we list the read tools Claudette may call, ask the server to expose only those, and deny everything else, including any tool the provider adds later.

Text Is Data, Not Instructions

Anything a model reads can contain instructions written by someone else: a message in a shared Slack channel, a web page, a line in a log. Everything Claudette reads is treated as data, and its instructions tell it never to act on requests in that data or send information anywhere.

That is a first line of defense, not a security boundary. We assume those instructions could one day fail, so the rest of the system is designed around what a fooled model could actually do. It cannot read a credential or change our infrastructure, and nothing it writes ships on its own: code arrives as a draft pull request an engineer has to merge, and documentation as a draft a person has to publish.

Network Access Is Constrained

The same principle applies to the network. For support, co-pilot, and documentation work, shell commands have no network access. A coding run can reach one host, our package registry, to install dependencies. Anything else is refused by the sandbox itself, not turned into a question the model could talk its way past.

Fetching web pages is limited to a short list: our own sites and the documentation of the tools we build with. We enforce that list ourselves rather than through Claude Code's domain rules. Those rules would also have opened every listed site to shell commands, so we kept the two lists separate.

We keep the hosting tool, the one connection where the model chooses the target, inside our own account. Before sending a request, it refuses any other account and any storage bucket that isn't ours. Without that, even a read-only call could have carried data into someone else's logs.

When something is blocked, nobody has to guess. The model is told why and not to retry, the event is logged, and a note under the Slack reply tells the person asking what was refused. So even if text Claudette reads manages to influence the model, it cannot choose a new destination and send data there.

Sign up for Crystallize newsletter!

For stories, strategies, and playbooks from the builders shaping the future of eCommerce architecture.

Checked Before Every Start

Everything above is verified, not assumed. Claudette refuses to run in a state it cannot vouch for, so before the orchestrator connects to Slack, it goes through the same sequence every time:

  1. Its configuration passes strict validation.
  2. The sandbox can start, and no directory holding secrets overlaps with what the model reads.
  3. The hosting permissions are tested against a list of write actions, from launching or terminating servers to deleting storage and creating access keys. If a single one is allowed, or cannot be verified, Claudette does not start.
  4. The database account's roles are checked. An unexpected role stops the startup.
  5. Every repository it may open pull requests on has a protected main branch.
  6. The documentation system answers, and its content format matches what Claudette expects.

Some failures stop the startup entirely, such as a write permission on hosting or an unprotected branch. Others only switch one feature off: if database monitoring is unreachable, Claudette starts without it. Every part of the orchestrator asks these checks, never the configuration, whether a capability is available, so something configured but not verified is simply off.

From a Conversation to a Plan

Many support conversations end with ‘so we should change this’. Claudette recognizes that moment. Once a conversation has converged on a concrete change, it writes a plan automatically: a title, summary, motivation, implementation steps, affected files and repositories, and a link back to the Slack thread. It can look up existing plans first, so it doesn't file the same change twice.

The plan is saved to our project management system, where it joins the rest of the team's work to be prioritized, discussed, and assigned like any other ticket. The conversation that found the problem becomes work the team can act on, without anyone having to write it up.

From a Plan to a Draft Pull Request

Nothing is implemented until an engineer explicitly requests a plan; the model can't start that work on its own. A coding session then writes tests, makes the change, and opens draft pull requests.

This is the only place the model writes code, so it gets the most protection:

  1. Main branches are protected on GitHub.
  2. The pull request token lives on a dedicated bot account with the minimum permissions, and cannot change CI workflows.
  3. The model works in its own copy of the code. The orchestrator fetches the result and pushes it from a separate copy the model cannot touch to an explicit destination.
  4. Any change to CI workflow files is refused.
  5. Every pull request opens as a draft, for an engineer to review and merge.
  6. The model's own attempt to push fails, because there is no credential where it works.

We know these boundaries matter because we broke earlier versions ourselves. We tested this design by attacking it. In an earlier version, a git hook planted in the model's copy could run on the host during the push, and changing the remote in that copy could redirect the push. Both attacks worked. Now the orchestrator never runs git inside the model's copy: it only fetches from it, in a way that ignores that copy's hooks and settings, and every git command the orchestrator runs has hooks disabled and a minimal environment. An automated test plants a hook on every build to prove it never fires.

Documentation Checked Against the Code

Our public developer documentation is managed in Crystallize itself. Claudette works through a documentation area page by page: it reads the current page, reads the code behind that feature, and proposes a corrected version. The orchestrator validates every proposal before anything is saved: only known content blocks, every image and video kept exactly as it was, and no internal terms. A proposal that fails goes back to the model with the reason. A valid one is saved as a draft by the orchestrator, and there is no publish operation at all: a person reviews and publishes.

Native to Slack

Claudette lives where our team already works: mention it in a channel, message it directly, or use a slash command. An investigation can take a minute or two, so it shows its progress live in the thread ("Loading the repository…", "Reading a file…") before replying. In a direct message, it can use the engineer's open conversation beside it as context, so a support engineer can ask, "What is going on here?" without copying anything.

A stop button cancels a request at any time.

Making a Large Codebase Readable

Our largest repository runs to millions of tokens, far more than a model can read at once. Early on, we discovered a particularly dangerous failure mode: only a tiny fraction of the repository was reaching the model, and nothing reported an error. Today, each repository gets a compact map, rebuilt automatically when its structure changes and checked against the real files, that tells the model where to look. It then reads the actual files it needs. Filtering out assets such as illustrations improved answer quality more than any prompt change.

The Stack

Claudette is written in TypeScript, from start to finish, and runs on Bun. Bun gives us the HTTP server, SQLite for conversation and usage records, process spawning, and the test runner, all without extra dependencies. bun build --compile turns the whole application, dashboard included, into a single executable with nothing to install besides it. Today that binary runs on our own server. Next, we want every engineer to run Claudette on their own machine, with the same tools and the same guarantees.

The intelligence comes from Anthropic: the Claude Agent SDK drives Claude Code for every request, with Claude Opus and Sonnet chosen by the kind of work. Everything Claudette can call is exposed as MCP tools, built with the Model Context Protocol. Our own Crystallize skills load into every session, the same ones we publish for developers building on Crystallize.

Around that:

  • Slack's Bolt framework and agent messaging experience for the conversations, with "Sign in with Slack" protecting the dashboard.
  • Zod validating every configuration value and every input at the edges.
  • Awilix wiring the services together, so each responsibility lives in one place and can be tested in isolation.
  • Seatbelt and bubblewrap sandboxing every shell command.
  • Repomix packing our repositories so the model can map them.
  • TypeScript 7's native compiler and the Rust-based Oxc tools (oxlint, oxfmt) keep type checks and linting fast enough to run on every change.

🤔What We Learned

  • Separate the orchestrator from the model. The application holds the credentials; the model works where none can be read.
  • Put every guarantee where the model cannot reach it: the operating system, the provider's permissions, the orchestrator.
  • Assume the model can be fooled, and limit what a fooled model could do.
  • Pick integrations based on how precisely you can limit them, and build your own tool when the off-the-shelf one is too broad.
  • Verify before you start and refuse to run unverified.
  • Keep a person on every action that matters: starting a code change, merging it, publishing a page.

Conclusion

In the last 90 days, we had 111 conversations with Claudette, close to 400 questions in total, and the last two weeks were the busiest yet, with more than 20 conversations a week. Most of that happens in the private co-pilot, next to a live customer thread. And it leads somewhere: those conversations produced 38 written plans, 29 of them bug fixes, ready for the team to pick up.

Claudette started as a way to stop interrupting engineers with questions the code could answer. It has become a daily part of how all our teams work, and a proving ground for the practices we share through our MCP server, skills, and live streams.

The biggest lesson has been simple: don't build security around the assumption that the model will behave. Build the system so that the important guarantees still hold when it doesn't.