Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
162685 stories
·
33 followers

European Law for .NET Developers: What the GDPR Means for Your Code

1 Share

Hey lovely readers,

The GDPR has been around since 2018, and most of us have heard of it. But when I ask developers what it actually means for the code they write, they often don't know. Usually someone mentions an annoying cookie banner and that's it.

So I'm starting a small series. I'm taking three European laws that affect developers and making them as simple as I can. This first post is about GDPR. After that, I'll cover the European Accessibility Act (EAA) and the AI Act.

Quick note before we start: I'm a developer, not a lawyer. Even though I'm really interested in this topic, I even did a thesis on it, I do not have a law degree. This post will help you understand what GDPR means for your code but if your company handles a lot of personal data, please also talk to someone who does this professionally.

What is GDPR?

GDPR (General Data Protection Regulation) is a European law that protects the personal data of people in the EU. It decides what you're allowed to collect, how you have to store it, and what people can ask you to do with their data.

Personal data is anything that can be linked to a real person. Names and email addresses are the obvious ones, but the list is longer than most people think:

  • A phone number or home address
  • A date of birth
  • An IP address
  • A user ID, if you can connect it back to someone
  • Location data

But not all personal data is the same. A first name or an email address is personal data, but some data is much more sensitive and risky when it leaks. GDPR calls these special categories:

  • Health data
  • Sexual orientation and sex life
  • Ethnic origin
  • Religious or philosophical beliefs
  • Political opinions
  • Union membership
  • Genetic data
  • Biometric data used to identify someone, like a fingerprint

For these categories, the starting point is different. You're not allowed to process this data at all unless one of a few exceptions applies, like the user giving explicit consent. And if you process a lot of it, you need a risk assessment called a DPIA (more on that later). Criminal records have their own, similar set of rules.

And why should you care as a developer? Because the fines are big. A company can be fined up to 20 million euros or 4% of its worldwide yearly revenue, whichever is higher. But honestly the biggest reason here is trust. Your users give you their data, and it's our job to treat it well.

Does it apply to me?

Probably yes. GDPR applies when your app handles personal data of people in the EU. It doesn't matter where your company is based. If you build a webshop in the United States and people in Germany can order from it then the GDPR applies to you. So if your app has user accounts, a contact form, a newsletter sign up, or even just logs with IP addresses in them, this post is for you.

But what if your project is really small? Maybe you have a SaaS with four users, or just a newsletter sign up on your blog. Then GDPR still applies. There is no minimum number of users, and no minimum revenue. One person from the EU in your database is enough.

But here's the good news. What GDPR asks from you grows with your size and your risk. A small project with simple data needs a small, simple setup. Let's look at two examples.

A small SaaS

Let's say you have a SaaS with four users who pay you a few euros a month. Here's what you do need:

  • A privacy policy. What you collect, why, how long you keep it, and who you share it with.
  • Agreements with the services you use. Your hosting and payment provider handle personal data for you. Most of them have a standard data processing agreement you can simply accept.
  • Decent security. HTTPS, hashed passwords, and backups.
  • A simple overview of your data. One page with what you store and why is enough. There's an exception for companies with fewer than 250 employees.
  • A way to handle requests. If someone asks to see or delete their data, you need to be able to do that.

And here's what you probably don't need:

  • A risk assessment, since your processing isn't high risk
  • A data protection officer
  • An EU representative, if your business is based in the EU
  • Special features in your code. If someone emails you to delete their account, doing it by hand is completely fine at this size.

A newsletter on your blog

Maybe you just have a blog with a newsletter sign up, and you store the email addresses in your own database. GDPR still applies. There is an exception for "purely personal" use, but a public newsletter isn't that.

Here's what you need:

  • Consent. Someone has to actively sign up. A double opt in where people confirm their email address is stored in your system is the best way to do this.
  • Proof of that consent. Store when and how someone signed up.
  • An unsubscribe link in every email.
  • A short privacy notice at your sign up form.
  • Deleting the data when someone unsubscribes.
  • An agreement with your email service if you use one to send your newsletter.

In code, that's a timestamp for the consent and an unsubscribe action. That's really it.

The rule of thumb

GDPR asks for results, not for features. When your project is small, a privacy policy, a simple overview of your data, and handling requests by hand is often enough. Build features for it once doing things by hand starts to hurt.

But what if you have a whole backend running with lots of users, well then there's some more work to do.

GDPR is more than code

Recently I helped a client who wanted to become GDPR compliant. When I wrote down all the work in tickets, something stood out: most of them weren't about code at all. They were about research and documentation.

So before we open our IDE, let's look at the work that comes first. You don't have to do all of this alone as a developer but you will be part of it. And honestly, you're often the person who knows best where the data actually lives.

Step 1: Find out what you have

  • Map all personal data you collect. Every table, every form field, every log should be on there. This becomes your data inventory.
  • Map how data flows. Where does it go after it enters your app? Think of your database, your email service, your analytics tool, and your payment provider.
  • Check what you receive from others. Partners or third parties might send you personal data too. That counts as well.

Step 2: Know why you have it

  • Write down the purpose for each type of data. Why do you store a phone number? "Maybe we'll need it someday" is not a purpose.
  • Pick a lawful basis for each purpose. GDPR has six of them, like consent, a contract with the user, or a legal obligation. Every purpose needs one.
  • Document legitimate interest when you use it. It's the most flexible basis, so you have to explain why your reason is fair, and why it doesn't hurt the rights of your users too much.

Step 3: Check the risk

  • Find your high-risk processing. Think of large amounts of sensitive data, tracking people, or making automatic decisions about them.
  • Decide if you need a DPIA. A Data Protection Impact Assessment is a document where you describe the risks and how you reduce them. For high-risk processing, it's required.

Step 4: Write it all down

This is the biggest part, and the least exciting one. But it matters, because GDPR asks you to be able to prove what you do.

  • Records of processing activities. An overview of what you process, why, and for how long.
  • A data retention schedule and a deletion policy. How long you keep each type of data and what happens after that.
  • A data breach response plan. If something leaks, you usually have to report it to the data protection authority within 72 hours. That's not the moment to start figuring out who does what. You want that decided and written down somewhere.
  • Procedures for user requests. What do you do when someone asks to see, change, or delete their data?
  • Consent documentation. If you use consent, you need to be able to show when and how someone gave it.
  • A vendor register and data processing agreements. Every service that handles personal data for you needs an agreement with you.
  • Privacy notices. One for your users, and don't forget your employees. Their data counts too.

Step 5: Check if you need a representative

If your company is not based in the EU but you do you do collect data from people that live in there, you might need to appoint an EU representative. The UK has its own version of GDPR after Brexit, so the same goes for the UK.

Where this meets your code

Here's the nice part. A lot of these documents turn straight into work for developers:

  • The data inventory tells you which properties to mark as personal data.
  • The retention schedule becomes a background job that deletes or anonymizes old data.
  • The user request procedures become a "download my data" and a "forget me" feature.
  • The breach response plan only works if you have good logging and monitoring.

So let's get to that code.

Keep personal data out of your logs

Let's start with the mistake I see the most. Something goes wrong in production, so someone adds this:

logger.LogInformation("User signed up: {@User}", user);

It works great for debugging. But now the user's name, email address, and maybe even their phone number are sitting in your logs. And logs get copied everywhere: to a log service, to a colleague's screen, to a support ticket. You lose control over that data very quickly.

.NET has a built-in way to fix this, with the Microsoft.Extensions.Compliance.Redaction package. You mark which data is personal, and .NET removes it from your logs for you.

First, install these packages:

dotnet add package Microsoft.Extensions.Compliance.Redaction
dotnet add package Microsoft.Extensions.Telemetry

Next, you create a small "taxonomy". That's just a fancy word for a list of the types of data you want to treat differently:

using Microsoft.Extensions.Compliance.Classification;

public static class DataTaxonomy
{
    public static string TaxonomyName => typeof(DataTaxonomy).FullName!;

    public static DataClassification PersonalInfo => new(TaxonomyName, nameof(PersonalInfo));
}

public class PersonalInfoAttribute : DataClassificationAttribute
{
    public PersonalInfoAttribute() : base(DataTaxonomy.PersonalInfo) { }
}

Now you can use that attribute on your model:

public class User
{
    public int Id { get; set; }

    [PersonalInfo]
    public string Name { get; set; } = string.Empty;

    [PersonalInfo]
    public string Email { get; set; } = string.Empty;
}

Then you turn on redaction in your Program.cs:

builder.Logging.EnableRedaction();

builder.Services.AddRedaction(redaction =>
{
    redaction.SetRedactor<ErasingRedactor>(
        new DataClassificationSet(DataTaxonomy.PersonalInfo));
});

The last step is logging with the logging source generator, so .NET knows which properties to look at:

public static partial class Log
{
    [LoggerMessage(Level = LogLevel.Information, Message = "User signed up")]
    public static partial void UserSignedUp(ILogger logger, [LogProperties] User user);
}

And you call it like this:

Log.UserSignedUp(logger, user);

Now the user's ID still shows up in your logs, so you can still debug. But the name and email are erased before they ever reach a log file. You get the information you need, without the personal data you don't.

Quick side note: ErasingRedactor removes the value completely. If you'd rather still be able to see that two log lines belong to the same person, you can use the HmacRedactor instead, which turns the value into a hash.

Protect sensitive data with the Data Protection API

Some data you do need to store, like a phone number. But that doesn't mean it should sit in your database as plain text. If someone ever gets access to your database, plain text is the worst possible situation.

How strong should the protection be?

Remember the special categories from the start of this post, like health data? You might expect GDPR to say exactly how you should encrypt that kind of data. It doesn't. GDPR asks for "appropriate" security, based on the risk. Encryption is mentioned as an example, but there's no required algorithm or key size.

In practice, it comes down to this: the more sensitive the data, the stronger the protection should be. For something like health data or sexual orientation, encryption is really the minimum. If that data leaks and it wasn't encrypted, then that's very hard to defend.

And there's a nice bonus. If data leaks but it was properly encrypted, you often don't have to inform every affected user because the data is useless to whoever has it.

Good to know: some sectors have extra rules on top of GDPR. In the Netherlands, for example, healthcare organizations have to follow a security standard called NEN 7510. So if you build apps for a specific sector, check if something like that applies to you too.

Encrypting data in .NET

ASP.NET Core comes with the Data Protection API, which can encrypt and decrypt values for you. Here's a small class that protects a phone number:

using Microsoft.AspNetCore.DataProtection;

public class PhoneNumberProtector(IDataProtectionProvider provider)
{
    private readonly IDataProtector _protector =
        provider.CreateProtector("Users.PhoneNumber");

    public string Protect(string phoneNumber) => _protector.Protect(phoneNumber);

    public string Unprotect(string protectedPhoneNumber) =>
        _protector.Unprotect(protectedPhoneNumber);
}

The string you pass to CreateProtector is called a purpose. Values protected with one purpose can't be unprotected with another, so your phone numbers and, say, your password reset tokens stay separated.

One important thing to know: the Data Protection API uses keys, and those keys need to be stored somewhere safe and permanent. If you lose them, you can't decrypt your data anymore. So make sure you configure where the keys live, for example:

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo("/keys"))
    .SetApplicationName("MyApp");

Keep in mind that Microsoft built this API mainly for data that doesn't live forever, like cookies and tokens. For sensitive data that you store for years, it's worth looking at options like Always Encrypted in SQL Server or keys in Azure Key Vault. But for getting started and understanding the idea, the Data Protection API is a great first step.

The right to be forgotten with EF Core

Under GDPR users can ask you to delete their personal data. This is called the right to be forgotten.

Sounds easy, right? Just delete the user. But here's where it gets interesting. Say this user has placed orders in your webshop. You often have to keep those orders for your bookkeeping. In the Netherlands, for example, that's seven years.

GDPR allows that. When the law says you have to keep something, then you can keep it. But you should remove everything that points to the person. That's where anonymizing comes in:

public async Task ForgetUserAsync(int userId)
{
    var user = await db.Users.SingleAsync(u => u.Id == userId);

    var orders = await db.Orders
        .Where(o => o.UserId == userId)
        .ToListAsync();

    foreach (var order in orders)
    {
        order.UserId = null;
        order.CustomerName = "Removed";
        order.ShippingAddress = "Removed";
    }

    db.Users.Remove(user);
    await db.SaveChangesAsync();
}

The user is gone, and the orders stay, but nobody can tell who placed them anymore.

Here's something I see very often in different web apps. Many apps use soft delete: an IsDeleted column that you set to true. That's great for "undo" buttons but it's not deleting. The data is still in your database. So if a user asks to be forgotten, a soft delete is not enough. And don't forget the places outside your main database: backups, logs, analytics tools, and third-party services you send data to. Those count too.

Anonymization vs pseudonymization

In the example above, I anonymized the orders. That word comes up a lot when you work with GDPR, and so does a word that sounds very similar: pseudonymization. They sound alike, but GDPR treats them very differently.

Pseudonymization: you can still go back

With pseudonymization, you replace something that identifies a person, like a name or an email address with a code. The information to link that code back to the person is stored somewhere else.

For example, your analytics tool only sees user_8f3a2c. A separate table or a key tells you who that is. This is still personal data, so GDPR still fully applies. But it lowers the risk a lot. If your analytics data leaks, nobody can see who user_8f3a2c is. That's why GDPR actively encourages it. The HmacRedactor I mentioned in the logging section is a good example. You can still see that two log lines belong to the same user, without seeing who that user is.

Anonymization: there's no way back

With anonymization, you change the data so nobody can find out who it belongs to anymore. Not your colleague, not a hacker, and not even you.

For example: "32 orders were placed in Amsterdam in March." There's no link to any customer at all. Data that is truly anonymous is no longer personal data. That means GDPR doesn't apply to it anymore.

The trap developers fall into

"Anonymous" is a high bar, and it's easy to think you've reached it when you haven't. Two common examples:

  • Hashing an email address. That's pseudonymization, not anonymization. Anyone with a list of email addresses can hash them too and look for matches.
  • Only removing the name. A postal code, a birth date, and a gender together can often point to one specific person. Each piece looks harmless, but together they're not.

This also goes for the "forget me" example above. Anonymizing the orders only works if nothing else on the order, like a postal code or a phone number, still points to the person.

Here's my advice after spending a lot of time thinking about this: if you're not 100% sure your data is anonymous, treat it as pseudonymized, and keep protecting it like personal data.

Only collect what you need

The easiest personal data to protect is the data you never collected. GDPR calls this data minimization.

A very common example is an endpoint that returns the full user entity:

app.MapGet("/users/{id}", async (int id, AppDbContext db) =>
    await db.Users.FindAsync(id));

That sends everything to the frontend, including the date of birth, the address, and the phone number, even if the page only shows a name. Let's fix that with a small DTO:

public record UserSummary(int Id, string DisplayName);

app.MapGet("/users/{id}", async (int id, AppDbContext db) =>
{
    var user = await db.Users
        .Where(u => u.Id == id)
        .Select(u => new UserSummary(u.Id, u.DisplayName))
        .SingleOrDefaultAsync();

    return user is null ? Results.NotFound() : Results.Ok(user);
});

Now only the data the page actually needs leaves your API.

The same idea works for your forms. Do you really need someone's full date of birth? Or do you only need to know that they're 18 or older? A simple checkbox might be all you need.

What about outside of Europe?

Before we wrap up, one more thing. GDPR is the most famous privacy law, but it's definitely not the only one. Many countries looked at GDPR and built something similar. Here are a few you might run into:

Where Law Good to know
United Kingdom UK GDPR After Brexit, the UK kept its own copy of GDPR. It's almost the same, with a few small differences.
Switzerland FADP Switzerland isn't in the EU, so it has its own law. It's very close to GDPR.
Brazil LGPD Strongly inspired by GDPR, with similar user rights and its own data protection authority.
California, USA CCPA Focuses a lot on letting users opt out of their data being sold or shared.
Canada PIPEDA Canada's federal privacy law for companies.
South Africa POPIA Also close to GDPR in how it treats personal data.
India DPDP Act One of the newer ones, focused on consent.

If you build your app with GDPR in mind then you're already in a good place for most of these laws. The ideas are often the same. Know what data you have, collect only what you need, protect it, and let users see and delete it.

But "similar" doesn't mean "the same". The details can be different like how users can say no, or how fast you need to report a data breach. So if your app has a lot of users in one of these countries, check that specific law too.

Your GDPR checklist

A short list you can bookmark:

  1. Map your personal data, where it goes, and why you have it.
  2. Make sure the documentation exists, and help turn it into code.
  3. Mark personal data in your code, and turn on redaction so it never ends up in your logs.
  4. Encrypt sensitive data you store, especially special categories like health data, and keep your keys safe.
  5. Build a real "forget me" flow: delete what you can, anonymize what you have to keep.
  6. If you're not sure data is truly anonymous, treat it as pseudonymized and keep protecting it.
  7. Remember that soft delete is not deleting.
  8. Use DTOs, and only collect and send the data you actually need.

That's a wrap!

GDPR can feel like a big scary law, but a lot of it comes down to good habits you can build right into your code. Start small, maybe with the logging part, and go from there.

In the next post of this series, I'll talk about the European Accessibility Act. It has been in force since June 2025, and it might apply to your app more than you think.

If you have questions, or tips on how you handle GDPR in your own projects, feel free to leave a comment or reach out to me on my socials.

See ya!

Read the whole story
alvinashcraft
15 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

AI-Generated Code Still Needs Human Review

1 Share

Just as you shouldn’t blindly copy and paste code from Stack Overflow, you shouldn’t blindly merge AI-generated code into your codebase. You still need to understand what it does, verify that it solves the right problem, and evaluate what else it might introduce.

Read the whole story
alvinashcraft
24 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Five Mistakes People Make with Agentic Factories

1 Share

I can ask an agent to do something, watch it produce good work, and still spend the next hour telling it what to do next. The agent is busy. So am I. Somehow, I’m still deciding every next step, with another conversation to attend to.

Moving into factory mode changes that relationship. I give the agent an outcome, the context it needs, and a way to check its work. It carries the task through, including dealing with problems along the way, and comes back with a result I can inspect.

I gave a Factory Life talk about five mistakes that get in the way of that shift. I’ve seen these patterns in my own factories, in my work with larger teams, and in conversations with other people building theirs.


and I are teaching Build Your Agentic Software Factory. If you’d like to build your own, there’s more about the course at the end.


What I Mean by a Factory

A factory is a repeatable way to get work done through agents. You provide direction; the agents have tools, context, and enough room to make decisions. They can work asynchronously in the background, while the surrounding system starts jobs and brings results or decisions back to you.

A factory carries intention through context and agent work to a checked result, then uses feedback to improve.

The work might be software, research, publishing, operations, or a mixture. I still decide what matters, what the agents are allowed to do, and whether the result is good enough. A failed check can send the work back for another pass. What we learn should improve the next run.

These choices depend on the task. Sometimes I want an interactive conversation, the strongest model earns its cost, or several agents are exactly what the work needs. The mistakes start when I make those choices by habit.

1. Micromanaging the Agent

This is an easy mistake to make because it feels responsible. I want the result to be right, so I stay close and keep supplying the next instruction.

Micromanagement can happen through constant chat or instructions that decide every step in advance.

There are two versions. In the first, I fall back into chat mode: the agent finishes a step, I give it another, and the plan stays in my head. In the second, I write such detailed instructions that the agent has little freedom to adapt when it learns something new.

I once put an “always write tests” rule in my standing agent instructions. When asked to change a README, an agent tried to write a test for the README. The agent followed the rule with rather more enthusiasm than the task called for.

Exploring a problem together is a good use of chat. But when I mean to delegate, the agent needs enough direction to complete a stretch of work without my constant input. If I keep wanting to intervene, it’s worth checking whether the brief leaves an important decision unresolved.

A delegation brief defines the outcome, context, boundaries, room to manoeuvre, and evidence of success.

A rich specification can give the agent plenty to work with. What should exist afterwards? What should it read? What must it preserve? Which decisions can it make? How will we know it worked?

For example, if I want a research brief to help decide what to teach, I can name the decision, relevant sources, how to handle uncertainty, and the requirement to return an unpublished draft. The agent can choose the search route and organise what it finds. I don’t need to choose every query or prescribe the paragraph count.

Hugo and I have talked about a good practical test: could I hand over this task and come back an hour later to the result I meant? That’s the level of delegation I’m aiming for. I still want checkpoints when a decision changes the outcome, cost, audience, or risk.

2. Using the Best and Most Expensive Model for Everything

I like capable models. When I’m exploring an idea or trying to understand something difficult, I often want the strongest one available to me. It’s tempting to make that the default for every job in the factory.

Background jobs keep running when nobody is watching the cost. They can use up a subscription allowance as well as a cash budget.

The talk’s chart compares measured capability and cost, with speed, reasoning effort, and task fit as further considerations.

Choosing a model also means choosing its reasoning effort: the setting for how much work it puts into thinking through a task. I also consider how long the job can take and how good the result needs to be. A fast response matters when I’m working interactively. A background task may have room to think longer, provided it still meets its deadline.

The useful comparison is what it costs to get a result I can accept. A cheaper attempt that needs repeated repairs may cost more overall. A larger model can earn its price by resolving a difficult question or catching a mistake before it spreads.

Different agents and tasks can use different models; compare accepted results, time, and total cost on your own work.

I choose by function and task. Brainstorming, analysis, planning, and review can benefit from deeper judgement. A well-defined task with a clear specification and checks is a good place to try a cheaper model. Difficult execution may still need the stronger one.

Benchmarks help me find candidates. Representative jobs from my own factory tell me whether those candidates fit. I compare the results against the same bar, including elapsed time, retries, my corrections, and spend or quota. If the cheaper choice keeps failing the check, I escalate.

3. Giving the Agent No Guidance, Taste, or Way to Check Its Work

An agent can read a repository or a collection of documents and still have no idea what good work looks like here. It doesn’t automatically know our settled decisions, preferred vocabulary, taste, or the exception that matters most.

An agent needs guidance, taste, and a way to check against local standards.

The agent will usually try to do something anyway. It fills the gaps with patterns it has learnt elsewhere and whatever context happens to be available. The result can be polished and coherent while missing the point of the project.

General competence only gets us so far. The knowledge that makes work fit a particular person or organisation often lives in their head, past conversations, and examples nobody has collected.

Standing instructions, references and examples, and supporting tools give context a durable home.

I try to give that context a home:

  • Standing instructions such as AGENTS.md hold decisions, constraints, and conventions that apply across tasks.

  • References and examples provide facts and show what I like, ideally with an explanation of why an example works.

  • Skills and supporting tools give the agent methods it can reuse, along with scripts, tests, and access to the relevant systems.

The current brief still needs to describe the task, and a worker on another machine needs access to the relevant instructions and references too.

More context isn’t automatically better. I want the relevant material to be easy to discover, without burying it in a manual full of things that don’t matter to this job. In Context Camp, I described how I organise and retrieve knowledge for my personal agents.

When the same mistake keeps happening, I ask where the process failed. Was the guidance missing, undiscovered, ignored, or contradicted? Did the agent have a way to recognise the problem? Adding another emphatic sentence to the instructions is only one possible response.

4. The Puppet Show

You start with a CEO agent, then add a strategist, a writer, an editor and a critic. Before long, you have an impressive organisational chart and a fair amount of conversation about the work.

I call this the puppet show: inventing a cast of personas before working out which functions need to be separated.

The puppet show: a cast of agent personas created before there is a functional need for them.

I used to dismiss multi-agent systems as anthropomorphising. I’ve changed my mind, and I now use multiple agents a lot. Separate workers can be very helpful when they handle distinct parts of the work.

Independent source gathering can run in parallel, and a reviewer can check the requirements and evidence. Some workers need different tools or narrower permissions. A background monitor needs to keep running over time, while a reviewer may be brought in for a single task.

Each extra agent needs its own context, a handoff, and a way to combine the results. Closely related tasks can become slower and less coherent when split unnecessarily.

Add agents by function when the work needs a different model, context, tools, access, or lifecycle.

I start with a capable agent and add functions as the need appears. Before adding another worker, I ask whether a better brief, more context, or different tools would let the existing agent do the job.

When I do split the work, I want to know what each agent receives, what it can do, what it returns, and how we’ll check the result. I also want to see whether the split helped. If it only adds handoffs, I can remove it. There is no prize for employing the most imaginary executives.

5. Treating Rules, Skills, and Prompts as Deterministic Infrastructure

The guidance in mistake three helps an agent make better decisions. Some limits also need to be enforced by the system around it.

Imagine a drafting agent with a publishing tool and credentials. Its instructions say “do not publish without approval”. The agent still has the capability to publish. We have asked a sentence to enforce a boundary.

Rules, skills, and prompts guide behaviour; prose alone cannot enforce access or exact checks.

Models interpret instructions. Their behaviour can vary with the model version, context, tools, and material they retrieve. I’ve seen odd behaviour after changing models and then realised that old instructions or skills needed another look. A page or email can also contain instructions that the agent should treat as untrusted material.

A skill can include a script that performs an exact check. That helps, but we still need to ask whether the agent can skip the script or continue after it fails. If it can, the check is optional in practice.

Enforce boundaries through permissions, separate drafting access, and checks that block the next action when they fail.

For things that must be enforced, I look at the system around the agent. File permissions and a sandbox can restrict which files it can reach. A drafting identity can save work while lacking permission to publish it. A required check can run before an action and block that action on failure.

The whole route needs checking. Hiding a Publish button does little if the agent can use the same credentials through another tool. A control can also be misconfigured. I want to check both that the worker can do what it’s allowed to do and that a forbidden action is blocked, using the tools it will actually work with.

There are two separate questions here: did the work pass the check, and is the agent authorised to take the next action? A correct draft doesn’t acquire permission to publish itself. Keeping those two questions separate lets the agent get on with the work while I retain control of what it’s allowed to do.

Five Questions for Your Next Task

Take a task that needed more supervision than you expected. Where did you keep intervening?

Five questions: delegate a result, fit the model, make the quality bar available, give agents distinct functions, and enforce boundaries.
  1. Am I delegating a result or directing every step?

  2. Does the model fit this part of the work?

  3. Can the agent find our quality bar and check the result?

  4. Does each agent have a distinct function?

  5. What enforces the boundary if the agent gets it wrong?

Change that part of the process and try again with work you can inspect. Stay close at first, then step back as the results and checks become dependable. That’s how I want my factories to grow: through experience with the work they’re doing.


Build Your Agentic Software Factory with Hugo and Me

These are some of the decisions and I will help you work through in Build Your Agentic Software Factory, our two-week course on Maven.

Build Your Agentic Software Factory with Hugo Bowne-Anderson and Eleanor Berger — Maven course social preview.

You’ll set up a factory, build a small tool you can use through conversation, extend that approach into a working web application, and learn how to check, publish, and share what your agents build. Bring an idea from your work or personal life, or use one of the starter projects. The aim is to leave with something you can use and a process you can repeat for the next idea.

We’ll work on writing specifications, giving agents context, deciding what to delegate, and inspecting the results. There are two live workshops, weekly office hours and project feedback, recordings, course materials, and a community of people building alongside you. Every student also gets $500 in Modal credits for running agent jobs or hosting what they build in the cloud.

You don’t need to know how to code. You do need curiosity, judgement, and a willingness to experiment and check the results. Experienced developers are welcome too; there’s plenty to learn about giving agents a larger share of the work.




Read the whole story
alvinashcraft
31 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Structured Extraction, Part 1: live semantic templates for unstructured documents

1 Share
Structured Extraction, Part 1: live semantic templates for unstructured documents
Read the whole story
alvinashcraft
38 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Experimenting with Claude Code Experimental Agent Teams

1 Share

This post was generated using Claude because, what better engine to consult about the nature and state of experimental Claude Code functionality? That said, I realize a number of readers take issue with using AI to assist writing. To help identify such content on this blog, I’ve introduced a new tag: “AI Slop.” This post is thus tagged.

set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1

One Line

You use Claude Code. One session. One context window. One worker grinding through your codebase in order.

Maybe you use subagents. Good. Subagents go do a focused thing and report back… like good and useful interns.

This post is about one line:

set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
claude

That line turns on agent teams.
And agent teams are not interns.
They are a team.

What Agent Teams Are

An agent team has four parts:

  • A team lead. Your main Claude Code session. It spawns teammates, builds the task list, and synthesizes results. In my experience, the Claude Code session replaces the orchestrator in a harness. (Assuming you’ve used – or even read about – agentic harnesses…)
  • Teammates. Each one is a full, independent Claude Code session with its own context window. Not a helper inside your session. A whole other Claude.
  • A shared task list. Tasks are pending, in progress, or completed. Tasks can depend on other tasks. A blocked task cannot be claimed until its dependencies finish. Teammates self-claim the next unblocked task when they finish one, and claiming uses file locking so two teammates don’t grab the same work.
  • A mailbox. Teammates message each other directly. By name. No routing everything through the lead.

That last part is the difference that matters.

Subagents Agent teams
Who talks to whom Report back to the caller Teammates message each other directly
Who coordinates The main session manages everything Self-coordination plus a shared task list
Best for Focused tasks where only the result matters Work that needs discussion, disagreement, and handoffs
Token cost Lower Higher. Each teammate is a separate Claude

You can also talk to any teammate yourself, without going through the lead.

Turning It On

The feature is experimental and off by default. Update Claude Code first (claude --version; agent teams need v2.1.32 or later, and newer is better).

Then pick your flavor.

Windows Command Prompt (this window only):

set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
claude

PowerShell (this window only):

$env:CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"
claude

bash / zsh:

export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
claude

Every session in one repo – put it in <repo>\.claude\settings.local.json

Note: Claude wrote “on Windows, %USERPROFILE%\.claude\settings.json“.
I disagree and use “on Windows, <repo>\.claude\settings.local.json” instead.
I can hear some of you thinking,

“Why <repo>\.claude\settings.local.json, Andy?”

That’s an excellent question. I’m so glad you asked!

%USERPROFILE%\.claude\settings.json would modify settings for every Claude Code session your user account runs on your / my image (laptop, VM, container, etc.). Adding a “.claude” subdirectory to your repo, and then adding a settings.local.json file to that subdirectory, will override settings from broader scopes – %USERPROFILE%\.claude\settings.json, which is the broadest scope for a user on an image, included.

Claude Code wrote this settings.local.json for me at first. (I later moved the flag into a launcher – more on that below.):

{
  "env": {
    "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
  }
}

The global (USER) settings file is tempting. Set it once, forget it. But it turns teams on for every session, and that has a cost I’ll get to in a minute. settings.local.json is scoped to one repo. Your other projects never see it. That turns out to be a feature.

A Note for Windows Folks

Agent teams have two display modes.

  • In-process (the default). All teammates run inside your main terminal. They show up in an agent panel below the prompt. Up and down arrows select a teammate. Enter opens its transcript so you can message it. Escape clears the selection, but while you’re viewing a teammate, it interrupts that teammate’s work. Ctrl+T toggles the task list. x stops a selected teammate. Works in any terminal.
  • Split panes. Each teammate gets its own pane. Requires tmux or iTerm2. Not supported in Windows Terminal or the VS Code integrated terminal. Durnit!

So on Windows, in-process it is. That’s fine. The panel is good.

You’ve Seen This Before

Here’s the part that makes me all giggly inside:

  • A lead that hands out work.
  • Workers that each run in their own isolated context.
  • A task list with dependencies, so step C waits on steps A and B. (i.e. orchestration, baby)
  • Quality gates that can refuse to let a task be marked complete.

If you’ve built an SSIS framework, you recognize these patterns:

  • It’s a controller package.
  • It’s child packages.
  • It’s precedence constraints and execution order stored as metadata.
  • It’s the recon (reconciliation) gate that won’t let the load proceed until the validation passes.

Claude Code even ships the gates. Hooks named TeammateIdle, TaskCreated, and TaskCompleted run at those moments, and exiting with code 2 sends feedback and keeps the work from moving forward.

That’s a precedence constraint with an opinion.

One real difference:
SSIS child packages don’t argue with each other.
Teammates do.

And that turns out to be the point. (Read more of my thoughts about playing nice with others.)

A First Team for Data Engineers

The documentation recommends starting with research and review, not code changes. That’s good advice. Parallel writes are where teams get into trouble.

So point a team at a folder of SSIS packages (they’re XML; Claude reads them just fine) and try something like this:

Spawn three teammates to review the SSIS packages in this folder.
Name them errors, performance, and deployment.
- errors: error handling, event handlers, logging
- performance: data flow design, lookups, blocking transformations
- deployment: parameters, connection managers, environment configuration
Have them challenge each other's findings before reporting.
Do not change any files.

Naming the teammates matters. You can address them by name later (“ask performance to look at Load_FactSales.dtsx again”).

The “challenge each other” line matters more. One reviewer finds one plausible problem and stops. Three reviewers trying to disprove each other find the problem that survives.

How I (I Mean, Claude Code) Set Mine Up

Confession first. My first attempt was the bash one-liner, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 claude, typed into a Windows command prompt. cmd told me it had never heard of that command. My second attempt was claude --teammates ux,backend,adversary. There is no --teammates option. Claude Code told me so, more politely than cmd.

You don’t list teammates on the command line. You turn the feature on, then ask for teammates in plain language.

Once I stopped guessing, I (I mean, Claude Code) set up my framework repo in three tiers. One rule drives all three: the expensive thing is opt-in.

Tier 1: The Everyday Session

Most work doesn’t need a team. It needs one capable session that doesn’t burn tokens. The local repo’s settings JSON file: <repo>\.claude\settings.local.json:

{
  "model": "sonnet",
  "advisorModel": "opus",
  "permissions": {
    "allow": [
      "Bash(git status)",
      "Bash(git diff:*)",
      "Bash(dotnet build:*)",
      "Bash(dotnet test:*)"
    ]
  }
}

Sonnet does the routine work. Opus typically weighs in as an advisor at decision points: before committing to an approach, on recurring failures, before declaring a task done. Pre-approving build and test commands stops the permission prompts for those commands during long runs. See Configure permissions for more information.

A word about those allow rules. They match command text, not intent.

Bash(dotnet build:*) approves dotnet build with any arguments. It does not approve dotnet.exe build. It does not approve dotnet build && something-else either, because Claude Code checks every part of a chained command, and every part has to match a rule.

And on Windows there’s a wrinkle. Bash rules cover the Bash tool. Claude Code also has a PowerShell tool, and PowerShell commands need their own rules: PowerShell(dotnet build *).

How do you know which one you need? Watch. When a command prompts, the prompt shows the tool and the exact command. That tells you which rule is missing.

Notice what’s missing. The agent teams flag is not in this file. That’s on purpose.

Tier 2: Specialist Subagents

Reusable roles live in <repo>\.claude\agents\, one markdown file each. I have three:

  • adversary (Opus, read-only plus Bash). A hostile reviewer. Assumes the change is wrong until proven otherwise. Stored in <repo>\.claude\agents\adversary.md.
  • verifier (Sonnet, Read and Bash only). Runs the build and tests. Reports exact pass/fail output. No interpretation. Stored in <repo>\.claude\agents\verifier.md.
  • scout (Haiku, read-only). “Find every place X is referenced.” Cheap lookups.
  • Stored in <repo>\.claude\agents\scout.md.

Here’s the adversary:

---
name: adversary
description: Use before any PR, merge, or release. Attacks the change: security, failure paths, contract violations, claims not backed by evidence.
tools: Read, Grep, Glob, Bash
model: opus
---
You are a hostile reviewer for this framework. Assume the change is wrong
until proven otherwise. Check: failure paths actually report failure; recorded
status matches observable reality; inputs crossing a platform boundary are
validated; nothing is marked done that wasn't run. Report findings by severity.
Do not edit files.

As subagents, they report a summary back to my session. Cheap. And the same files work as teammate roles when I do want a team.

One thing a ‘guide’ I found online got wrong: don’t hand-write anything in %USERPROFILE%\.claude\teams or %USERPROFILE%\.claude\tasks. That’s runtime state. Claude Code generates it and overwrites your edits.

Roles go in <repo>\.claude\agents\.

Tier 3: Teams, On Purpose

Here’s the cost I promised. Claude names subagents on its own so it can message them later. With the agent teams flag on, a named subagent launches as a teammate. So with the flag in any settings file, teams can form when you didn’t ask for one. Every teammate is a full session. You pay for each.

So the flag lives in a launcher. team.cmd in the repo root turns teams on and makes Opus the lead:

@echo off
setlocal
set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
claude --model opus %*

Plain claude gets me Tier 1, no teams. team gets me teams with an Opus lead. Then I ask for a team:

Create an agent team with three teammates.
Name them ux, backend, and adversary; use the adversary agent type for adversary.
ux focuses on the user experience of [the thing you're working on].
backend focuses on implementation and architecture.
adversary tries to break or disprove whatever the other two propose.
Use Sonnet for ux and backend.

Teammates don’t see my conversation with the lead. The spawn prompt is all the task context they get, so I put the task in it.

Where teams earn their keep in my repo: platform adapters live in separate files, so one teammate per adapter can build in parallel without overwriting each other. And design debates, where backend and adversary argue over an execution-model decision.

Guardrails

  • Keep CLAUDE.md short. Every teammate and subagent loads it. Every extra line gets paid for once per agent. Build commands, conventions, and the “adversary review before PR” rule go there. Everything else goes in docs Claude reads when it needs them.
  • Hooks, not rules, where it matters. Use a TaskCompleted hook that runs the tests and exits with code 2 on failure. The task cannot be marked complete while tests fail.

A CLAUDE.md rule asks.
A hook enforces.
Data engineers know the difference between a convention and a constraint.

Best Practices (and Things That Will Bite You)

It’s experimental. Experimental means some of this:

  • Tokens. Every teammate is a separate Claude with its own context window. Cost scales with team size. Start with three to five teammates. Three focused teammates beat five scattered ones.
  • Same-file edits. Two teammates editing one file means overwrites. Give each teammate its own files.
  • Teams you didn’t ask for. With CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS set to 1, a subagent Claude names on its own launches as a teammate. If you want plain subagents back, set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS to 0.
  • Interactive only. Run claude -p (headless, including Agent SDK sessions) and you get subagents, not teammates.
  • Resume doesn’t bring them back. /resume and /rewind don’t restore in-process teammates. The lead may try to message teammates that no longer exist. Tell it to spawn new ones.
  • One team per session. No nested teams. The lead is the lead for life.
  • Permission prompts land on the lead. Pre-approve the common stuff in your permission settings or you’ll be clicking a lot.
  • The lead gets impatient. Sometimes it starts doing the work itself instead of waiting. Tell it: “Wait for your teammates to complete their tasks before proceeding.” Sometimes it declares victory early. Tell it to keep going.

I have managed people. This list is familiar.

The Verdict

Software engineering spent years teaching AI to work alone. Now it’s teaching AI to work as a team: a lead, a task list, dependencies, gates, and a way to say “I think you’re wrong.”

Data engineering called that orchestration. We’ve been running it in production for twenty years.

One line to try it.

Andy :{>

This feature is experimental and changing fast.
Details current as of the date written (10 Oct 2026).
The source of truth is the
Claude Code agent teams documentation.

Read the whole story
alvinashcraft
57 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

How to Clear the SSMS Cache (SSMS 21 and Later)

1 Share

How to Clear the SSMS Cache (SSMS 21 and Later)

Updated for SSMS 21 and later: This is a new, rewritten version of my earlier article for SQL Server Management Studio (SSMS) 21 and later. SSMS 21 is built on Visual Studio 2022 and is now a native 64-bit application. So it now installs to C:Program Files instead of C:Program Files (x86) by default. It also uses the Visual Studio Installer. 

Some of the folders and files that older guides, including the original version of this one, tells you to move them, aren't where they used to be. If you're still on SSMS 20 or earlier, the original article still applies.

If you are reading this, it's probably because you too have this rare need to clear the SSMS cache for some kind of troubleshooting.

When I'm troubleshooting a connection issue that might be caused by a cached or stale connection, the first things that come to mind are the DNS client cache and, occasionally, the ARP cache. The SSMS cache rarely makes the list. But there are times when it's exactly the right suspect: a new table that IntelliSense refuses to acknowledge, an old server name that keeps showing up in the connection dialog, or SSMS just behaving oddly after an upgrade.

"The SSMS cache" is really several caches

Almost everything in the computer world uses some kind of cache, and SSMS is no exception. The catch is that "the SSMS cache" isn't one thing. It's at least four, and they're cleared in different ways:

What's cached What it does for you How to clear it
IntelliSense object metadata Table, column, and function names for completion lists Ctrl+Shift+R in the query window
Connection history (MRU, most recently used) Server names, authentication types, and user names in the connection dialog Remove the entry in the connection dialog, or reset the per-user data
Environment settings Fonts, colors, window layouts, and everything under Tools > Options Tools > Import and Export Settings > Reset all settings
Microsoft Entra ID tokens (SSMS 22) Avoids reauthenticating on every connection Help > Clear Entra ID Token Cache

Refresh The Stale IntelliSense

IntelliSense helps you by keeping a cached copy of database metadata (information about objects such as tables, columns, and functions), so it can auto-complete names as you type without asking the server every time.

The trade-off is that the copy can fall behind. IntelliSense doesn't instantly know about database objects created by another connection after your editor window connected to the database. So if a colleague adds a table while your query window is open, you may get a red squiggle under a perfectly valid name. There are three ways to refresh the object cache in your active SSMS window:

  • Press Ctrl+Shift+R.
  • Select Edit > IntelliSense > Refresh Local Cache.
  • Disconnect the query window and reconnect.

You may also see the lag after your own DDL in the same window. If you do, the fix is the same.

If a refresh doesn't help, the problem probably isn't the cache. The other typical culprits are 1) If you have SQLCMD mode enabled, it turns IntelliSense off 2) a syntax error above the cursor can stop parsing 3) a lost connection breaks completion lists and 4) objects you don't have permission to see won't appear in them. No amount of cache clearing can grant you permissions.

Curious what IntelliSense actually asks the server? Capture your own session with Extended Events (or Profiler). The metadata queries typically show up with the client application name Microsoft SQL Server Management Studio - Transact-SQL IntelliSense under your login.

Removing one unwanted server in the connection list

If you just want SSMS to forget a single old server name, you don't need to clear anything else. Open the Server name dropdown, hover over the entry, and press Delete. 

SSMS 21 also introduced a Modern connection dialog with its own Recent and Favorites lists, so the exact controls depend on which dialog you have enabled under Tools > Options > Environment > Connection Dialog.

If the problem is a login failure right after you were added to a Microsoft Entra ID group, that's a token cache issue, not a connection-history one. Use Help > Clear Entra ID Token Cache. That menu item was introduced in SSMS 22.0, so don't go looking for it in SSMS 21.

Save your settings before you clear anything

Your customizations (basically everything under Tools > Options) live in a Visual Studio settings file. In older SSMS versions, guides pointed to NewSettings.vssettings under a version folder. In SSMS 21 and later, I'd rather not hard-code a path, because Microsoft doesn't document one for these releases. The documented method to backup/export settings is to use the built-in Import/Export Settings wizard:

  1. Select Tools > Import and Export Settings.
  2. Choose Export selected environment settings and save the .vssettings file somewhere safe.

The same wizard has a Reset all settings option. If odd editor or layout behavior is your only complaint, try that before moving or deleting any folders.

If you use Registered Servers, export them too: View > Registered Servers, right-click the group, then Tasks > Export. User names and passwords are excluded by default. SQL Server Authentication passwords are stored per user, so you'll have to reenter them after an import.

Clearing the SSMS cache folders

When the targeted fixes don't help and you suspect damaged per-user files, close all SSMS instances, copy RegSrvr*.xml if you want to keep your Local Server Groups, remove the files in these two folders, and restart SSMS (Microsoft Learn: Clear SSMS cache files):

%USERPROFILE%AppDataLocalMicrosoftSQL Server Management Studio
%USERPROFILE%AppDataRoamingMicrosoftSQL Server Management Studio

SSMS 21 and later have a wrinkle here. These releases also keep version-specific data under MicrosoftSSMS. Microsoft documents %APPDATA%MicrosoftSSMS<installid> as the SSMS 22 location for activity logs. Under %LOCALAPPDATA%MicrosoftSSMS you'll typically find one subfolder per installed release, named something like 21.0_xxxxxxxx, and these appear to hold per-user data such as connection history. Microsoft hasn't document their contents, and its cleanup procedure doesn't mention them. So, on SSMS 21 and later, clearing only the two documented folders may leave your connection history untouched. The suffix differs from machine to machine, so look before you move or delete them.

My advice is to move these folders instead of deleting them. A move gives you the same clean start, plus an undo button.

  1. Export your settings and registered servers (see above).
  2. Close every SSMS window.
  3. Move the folders to a backup location. Keep Local and Roaming separate.
  4. Start SSMS and repeat the action that was failing.
  5. Restore only what you need. If you put everything back right away, you'll never know whether the cache was the problem.

When SSMS starts with empty folders, it rebuilds them. You may see first-run prompts or messages about missing settings. If a message isn't what you expected, read it and write it down; don't just click through.

One security note: contents inside some files aren't plain text, but that doesn't make them harmless. They hold your connection details and, if you've used Remember Password, saved credentials. Saved passwords are encrypted and tied to your Windows account, so they aren't readable as plain text. Even so, treat your backup copies like any other file that holds credentials: keep them in your own profile, and delete them once you no longer need them.

A PowerShell script to back up and clear the cache

Here is the script I use to back up and clear the SSMS cache folders. By default it runs as a dry run: it shows what it would move and changes nothing. Add -NoDryRun to actually move the folders.

Out of the box, it targets only the two folders in Microsoft's procedure. Add -IncludeSsmsData to also move MicrosoftSSMS in Local and Roaming. That resets connection history and per-user data for every SSMS 21+ release you have installed, not just one.

Before you run it:

  • Use Windows PowerShell 5.1 or PowerShell 7 on Windows, as the same Windows account that runs SSMS. It doesn't connect to SQL Server, so no database permissions are involved.
  • Close SSMS. The script refuses to run if Ssms.exe is running.
  • Keep the backup on the same drive as your profile. Move-Item can't move a folder to a different volume (Microsoft Learn: Move-Item), so the script stops if your profile is redirected to another drive or a network share.
  • Before it moves anything, the script writes HOW-TO-RESTORE.txt to the backup folder. The file lists each original path, explains how to restore by hand, and includes PowerShell code that puts the folders back.
  • If something fails partway, the script stops and doesn't roll back. The restore notes still cover you: the restore code puts back whatever was moved and skips anything that wasn't.
Backup-SsmsCache.ps1
#requires -Version 5.1
<#
.SYNOPSIS
    Backs up AND CLEARS SQL Server Management Studio (SSMS) per-user folders.

.DESCRIPTION
    Verifies SSMS (Ssms.exe) is not running, then MOVES SSMS per-user folders
    from AppData to a timestamped backup folder. Moving the folders clears them
    while keeping a copy you can restore from.

    Default targets are the two folders in Microsoft's "Clear SSMS cache files"
    procedure:
        %LOCALAPPDATA%MicrosoftSQL Server Management Studio
        %APPDATA%MicrosoftSQL Server Management Studio

    -IncludeSsmsData also moves the folders SSMS 21 and later use for
    version-specific data, such as connection history:
        %LOCALAPPDATA%MicrosoftSSMS
        %APPDATA%MicrosoftSSMS
    This resets EVERY installed SSMS 21+ release for the current user.

    Before moving anything, writes HOW-TO-RESTORE.txt to the backup folder.
    It lists the original paths, explains how to restore, and includes
    PowerShell code that puts the folders back.

    Runs in DryRun (preview) mode by default. Use -NoDryRun to make changes.
    This is not an IntelliSense refresh. For stale IntelliSense, press
    Ctrl+Shift+R in the query window instead.

.PARAMETER NoDryRun
    Perform the move. Without this switch, nothing is changed.

.PARAMETER IncludeSsmsData
    Also move the SSMS 21+ "MicrosoftSSMS" folders (see DESCRIPTION).

.PARAMETER BackupRoot
    Optional local-drive folder for the backup. Defaults to
    DocumentsSQL Server Management Studio - Backup. Must be on the same
    drive as the folders being moved, because Move-Item cannot move a
    folder to a different volume.

.NOTES
    Updated: 2026-10-05
    Version: 2.1
    Status:  Tested on Windows; worked as expected. Further testing
             planned. Always run the dry run first.

.EXAMPLE
    .Backup-SsmsCache.ps1
    Previews the operation (DryRun default).

.EXAMPLE
    .Backup-SsmsCache.ps1 -NoDryRun
    Backs up and clears the two Microsoft-documented folders.

.EXAMPLE
    .Backup-SsmsCache.ps1 -IncludeSsmsData -NoDryRun
Also backs up and clears the SSMS 21+ version-specific data folders, for every SSMS 21+ release installed for the current user. This is close to a fresh-install state but not guaranteed to match it; some state, such as Entra ID or GitHub sign-in, may live elsewhere.
#> [CmdletBinding()] param( [switch]$NoDryRun, [switch]$IncludeSsmsData, [string]$BackupRoot ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Assert-SsmsClosed { if (Get-Process -Name 'Ssms' -ErrorAction SilentlyContinue) { throw 'SSMS is running. Close every SSMS window, then run this script again.' } } $mode = if ($NoDryRun) { 'LIVE' } else { 'DRY RUN' } Write-Output "[$mode] SSMS per-user folder backup + clear" Assert-SsmsClosed if (-not $env:LOCALAPPDATA -or -not $env:APPDATA) { throw 'LOCALAPPDATA or APPDATA is not set. Nothing was changed.' } # --- Backup location ---------------------------------------------------------- if (-not $BackupRoot) { $BackupRoot = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'SQL Server Management Studio - Backup' } if ($BackupRoot -notmatch '^[A-Za-z]:\') { throw "BackupRoot must be a local-drive path such as C:SsmsBackup. Got: $BackupRoot" } $BackupRoot = [IO.Path]::GetFullPath($BackupRoot) $BackupPath = Join-Path $BackupRoot (Get-Date -Format 'yyyyMMdd_HHmmss') # --- Build the list of folders to move ---------------------------------------- $candidates = @( @{ Label = 'LocalSQL Server Management Studio'; Source = Join-Path $env:LOCALAPPDATA 'MicrosoftSQL Server Management Studio' } @{ Label = 'RoamingSQL Server Management Studio'; Source = Join-Path $env:APPDATA 'MicrosoftSQL Server Management Studio' } ) if ($IncludeSsmsData) { $candidates += @{ Label = 'LocalSSMS'; Source = Join-Path $env:LOCALAPPDATA 'MicrosoftSSMS' } $candidates += @{ Label = 'RoamingSSMS'; Source = Join-Path $env:APPDATA 'MicrosoftSSMS' } } # Validate everything before moving anything. $plan = @( foreach ($c in $candidates) { if (-not (Test-Path -LiteralPath $c.Source -PathType Container)) { continue } $src = [IO.Path]::GetFullPath($c.Source) if ([IO.Path]::GetPathRoot($src) -ine [IO.Path]::GetPathRoot($BackupRoot)) { throw "Different drive or redirected profile: $src. Choose a -BackupRoot on the same drive or move this folder manually." } if ($BackupRoot.StartsWith($src.TrimEnd('') + '', [StringComparison]::OrdinalIgnoreCase)) { throw "BackupRoot cannot be inside a folder being moved: $src" } [pscustomobject]@{ Label = $c.Label Source = $src Destination = Join-Path $BackupPath $c.Label } } ) if ($plan.Count -eq 0) { Write-Warning 'None of the targeted SSMS folders exist. Nothing to clear.' return } Write-Output "Backup + clear target: $BackupPath" foreach ($p in $plan) { Write-Output " $($p.Label)" Write-Output " From: $($p.Source)" Write-Output " To: $($p.Destination)" } if (-not $NoDryRun) { Write-Output "`n[DRY-RUN COMPLETE] Nothing was changed. Use -NoDryRun to execute." return } # --- Restore notes ------------------------------------------------------------ # Wraps a path in single quotes for the generated code, doubling any embedded # single quote (for example, a user profile named O'Brien). function ConvertTo-QuotedLiteral([string]$Text) { "'" + $Text.Replace("'", "''") + "'" } $RestoreFile = Join-Path $BackupPath 'HOW-TO-RESTORE.txt' $folderLines = foreach ($p in $plan) { " @{{ Backup = {0}; Original = {1} }}" -f (ConvertTo-QuotedLiteral $p.Destination), (ConvertTo-QuotedLiteral $p.Source) } $nl = [Environment]::NewLine $folderList = foreach ($p in $plan) { " $($p.Label)$nl Original: $($p.Source)$nl Backup: $($p.Destination)" } $restoreNotes = @" SSMS CACHE BACKUP - HOW TO RESTORE ================================== Created: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Computer: $env:COMPUTERNAME User: $env:USERDOMAIN$env:USERNAME The following folders were scheduled to be MOVED here from AppData: $($folderList -join $nl) If the backup script reported an error, some folders may not have moved. A folder was moved only if it exists under this backup folder. BEFORE YOU RESTORE ------------------ - Restore as the same Windows user shown above. - Close every SSMS window. - Consider restoring only what you need. If the cleanup fixed your problem, restoring everything may bring the problem back. - This folder can contain saved connection details and credentials. Keep it private, and delete it once you no longer need it. RESTORE BY HAND --------------- For each folder listed above: 1. If SSMS recreated a folder at the Original path, rename it (for example, add .recreated to the name). Do not merge the two. 2. Move the Backup folder back to the Original path. 3. Start SSMS and confirm your settings and connections are back. RESTORE WITH POWERSHELL ----------------------- Copy everything between the two marker lines into a PowerShell window (or save it as Restore-SsmsCache.ps1 and run it). It restores every folder that is still in this backup and renames any folder SSMS recreated to <name>.recreated_<timestamp>. It does not delete anything. # ----- BEGIN RESTORE CODE ----- `$ErrorActionPreference = 'Stop' if (Get-Process -Name 'Ssms' -ErrorAction SilentlyContinue) { throw 'SSMS is running. Close every SSMS window, then run this again.' } `$stamp = Get-Date -Format 'yyyyMMdd_HHmmss' `$folders = @( $($folderLines -join $nl) ) foreach (`$f in `$folders) { if (-not (Test-Path -LiteralPath `$f.Backup)) { Write-Warning "Not in backup (never moved, or already restored): `$(`$f.Backup)" continue } if (Test-Path -LiteralPath `$f.Original) { `$asideName = (Split-Path `$f.Original -Leaf) + '.recreated_' + `$stamp Rename-Item -LiteralPath `$f.Original -NewName `$asideName Write-Output "Renamed folder SSMS recreated to: `$asideName" } Move-Item -LiteralPath `$f.Backup -Destination `$f.Original Write-Output "Restored: `$(`$f.Original)" } Write-Output 'Restore complete. Start SSMS and check your settings and connections.' # ----- END RESTORE CODE ----- "@ # --- Move --------------------------------------------------------------------- $moved = @() try { New-Item -Path $BackupPath -ItemType Directory -Force | Out-Null # Write the restore notes BEFORE moving anything, so they exist even if a move fails. Set-Content -LiteralPath $RestoreFile -Value $restoreNotes -Encoding UTF8 Write-Output " Restore notes: $RestoreFile" foreach ($p in $plan) { Assert-SsmsClosed New-Item -Path (Split-Path $p.Destination) -ItemType Directory -Force | Out-Null Move-Item -LiteralPath $p.Source -Destination $p.Destination $moved += $p.Label Write-Output " MOVED: $($p.Label)" } } catch { Write-Warning "Stopped early. Folders already moved: $(if ($moved) { $moved -join ', ' } else { 'none' })" Write-Warning "Nothing was rolled back automatically. See $RestoreFile to restore." throw } Write-Output "`nSSMS folders backed up and cleared to: $BackupPath" Write-Output "To undo, follow the instructions in: $RestoreFile" Write-Output 'Start SSMS and retest the original problem before restoring anything.'

Here's what a successful run looks like with the default targets. Your user name appears where <UserName> is shown, and the timestamp folder name reflects when you ran the script:

Example output
PS> .Backup-SsmsCache.ps1 -NoDryRun[LIVE] SSMS per-user folder backup + clearBackup + clear target: "C:Users<UserName>DocumentsSQL Server Management Studio - Backup20261010_035754LocalSQL Server Management Studio"
From: "C:Users<UserName>AppDataLocalMicrosoftSQL Server Management Studio" To: "C:Users<UserName>DocumentsSQL Server Management Studio - Backup20261010_035754LocalSQL Server Management StudioRoamingSQL Server Management Studio"
From: "C:Users<UserName>AppDataRoamingMicrosoftSQL Server Management Studio" To: "C:Users<UserName>DocumentsSQL Server Management Studio - Backup20261010_035754RoamingSQL Server Management Studio"
Restore notes: "C:Users<UserName>DocumentsSQL Server Management Studio - Backup20261010_035754HOW-TO-RESTORE.txt"
MOVED: LocalSQL Server Management Studio MOVED: RoamingSQL Server Management Studio SSMS folders backed up and cleared to: "C:Users<UserName>DocumentsSQL Server Management Studio - Backup20261010_035754"
To undo, follow the instructions in: "C:Users<UserName>DocumentsSQL Server Management Studio - Backup20261010_035754HOW-TO-RESTORE.txt"
Start SSMS and retest the original problem before restoring anything.

To undo the cleanup, open HOW-TO-RESTORE.txt in the backup folder. Close SSMS, then either follow the manual steps or copy the code between the BEGIN RESTORE CODE and END RESTORE CODE markers into a PowerShell window. The restore code renames any folder SSMS recreated to <name>.recreated_<timestamp> before moving your backup back. It deletes nothing and never merges the old and new folders. Once you're happy, you can delete the .recreated_ folders and the backup yourself.

A few configuration files worth knowing

Some file names come up in nearly every SSMS cache discussion. Here's a short guide for SSMS 21 and later:

  • .vssettings files: Visual Studio-format settings files that store fonts, colors, layouts, and editor preferences. Don't go hunting for the file; export and import it through Tools > Import and Export Settings.
  • RegSrvr*.xml: Your Local Server Groups in Registered Servers, which you build and maintain yourself. Keep a copy before clearing the cache folders. A .regsrvr export is the cleaner backup.
  • Connection history: Unlike registered servers, this list maintains itself: SSMS adds every server you connect to. In SSMS 18 through 20 it lived in UserSettings.xml. In SSMS 21 and later it appears to live under the version-specific MicrosoftSSMS folders instead.
  • Ssms.exe.config: An application configuration file that sits in the installation folder next to ssms.exe (by default C:Program FilesMicrosoft SQL Server Management Studio 21ReleaseCommon7IDE for SSMS 21), not in your profile. It isn't a cache file. Leave it out of any cleanup.

The short version

If IntelliSense is behind, press Ctrl+Shift+R. To get rid of one old server name, delete it from the connection dialog. For a sign-in problem after an Entra ID group change, clear the token cache. Clear the cache folders only when nothing narrower works, and move them rather than delete them so you can always get back to where you started.

The post How to Clear the SSMS Cache (SSMS 21 and Later) appeared first on SQLServerCentral.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories