Skip to main content

Posts

Showing posts from August, 2026

Turning a single work session into a reusable Skill with Microsoft's Skill Recorder

In my effort to adopt an 'AI first' philosophy, I spend a lot of time automating my day-to-day activities and transforming them to skills. It's not the most exciting activity: I know exactly how to do a task, I've done it a dozen times by hand, but writing it down as a clean SKILL.md for an agent takes almost as long as just doing the task again. Microsoft created an open-source a tool that skips that step entirely: Skill Recorder . The idea is simple: you record yourself doing the task once, and it generates the skill for you. Sounds good? Let’s give it a try… What it actually does Skill Recorder is a desktop app (Electron, macOS-first with Windows 11 support) that captures a real work session on your screen: clicks, app and window switches, the pages you visit, clipboard snippets, and optionally your spoken narration. Nothing leaves your machine while you're recording. Capture, storage, frame extraction, and narration transcription all happen locally. T...

Packaging skills in a plugin, this time for GitHub Copilot CLI

Note: this post is part of a series. Part one covered packaging a skill in a plugin for Claude Code . Same idea, different tool, this time focussing on GitHub Copilot. After writing that first post I got the obvious follow-up question: does this also work for GitHub Copilot? Short version: yes, SKILL.md itself is portable, but the plugin wrapper isn't. Copilot CLI has its own plugin format, laid out slightly differently from Claude Code's. What's the same? A skill is still just a folder with a SKILL.md inside: YAML frontmatter ( name , description , optionally license ), then a Markdown body with instructions. Copy that file as-is between tools — nothing about the skill content needs to change. What's different? Where Claude Code wants the manifest tucked inside .claude-plugin/plugin.json , Copilot CLI puts plugin.json straight at the plugin root: api-testing-plugin/ ├── plugin.json # Required manifest, at the root ├── agents/ #...

Packaging skills into a plugin

It didn't take long until skills became a key element in our agentic development workflow. At that moment a skill stopped being "that SKILL.md I keep in one project" and became something we want on every machine, for every teammate, without copy-pasting a folder around. That's the point where you package it into a plugin. A plugin is a container: it bundles one or more skills plus optionally commands, agents, hooks, and MCP servers into a single installable unit with a manifest. Skills are the "what to do", plugins are the "how you get it onto someone else's machine". Our first approach: using git submodules Our first approach was through centralizing all our skills in a separate Git repo and share it between projects by using git submodules. That approach worked but felt more like a hack than a final solution. The better approach: wrap it in a plugin A plugin is just a directory with a manifest and a conventional layout: my-plugin/...

Azure DevOps Server – Git submodule support

Yesterday I was talking about git submodule support in Visual Studio and that reminded me of something else: Azure DevOps Server got submodule support too. It's a small feature, but a genuinely useful one. Why do we need this? If you've worked with submodules before, you know the annoying part isn't adding them, it's navigating them afterwards. In the Files hub, a submodule used to just show up as a folder reference. To actually browse its content, you had to know the submodule's repo, open it separately, and find the right commit yourself. What changed Azure DevOps Server now has UI support for Git submodules in the Files hub. You can click straight into a submodule from your project and land exactly on the commit that's referenced there. No more manually tracking down the right repo and commit by hand. It works across multiple Git services that a submodule can point to: Azure Repos GitHub GitLab Bitbucket Multiple .gitmodules URL...

Visual Studio finally gets proper Git submodule support

Git submodules wasn't a feature I used that often. Until we started to use it as a way to share context( agent.md ) and skills between our projects. That's the moment we discovered that Git submodule support was missing in Visual Studio and we had to fall back to the terminal typing git submodule update --init --recursive With Visual Studio 18.9, that finally changes. Submodules are now a first-class part of the Git experience in the IDE. What was missing Submodules are genuinely useful. They let you pull a shared library, an SDK, or a set of build scripts into your repository without copy-pasting code around. You could fall back to the command line, but the tooling was missing. Visual Studio would show you a submodule as a plain folder, with no indication of its state, no way to add or remove one from the UI, and no understanding of how it related to the parent repository. Every actual submodule operation meant tabbing out to the command line. What you get now Open a...

Renovate on Azure DevOps: picking the right work item type

After our fix yesterday, we got our Renovate pipeline up-and-running. However the next hurdle showed up quickly. Right after the SSL certificate fix, I got the pipeline green, but the log showed this warning: WARN: Azure: work item type does not exist in project (or the token lacks permission to it); skipping issue. The Dependency Dashboard needs a process that defines this work item type. Set one your project defines via the `azureWorkItemType` repo config option. (repository=Framework en Tooling/SOFACore) "workItemType": "Issue", "availableTypes": [ "Bug", "Task", "Quality of Service Requirement", "Scenario", "Risk", "Code Review Request", "Code Review Response", "Feedback Request", "Feedback Response", "User Story", "Test Case", ...

Fixing "UNABLE_TO_VERIFY_LEAF_SIGNATURE" when running Renovate against an internal Azure DevOps server

Recently I ran into a failing Renovate pipeline right after configuring it against our internal Azure DevOps server: "err": { "code": "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "message": "unable to verify the first certificate; if the root CA is installed locally, try running Node.js with --use-system-ca" } Error: unable to verify the first certificate; if the root CA is installed locally, try running Node.js with --use-system-ca at TLSSocket.onConnectSecure (node:internal/tls/wrap:1748:34) at TLSSocket.emit (node:events:509:28) at TLSSocket.emit (node:domain:489:12) at TLSSocket._finishInit (node:internal/tls/wrap:1185:8) at TLSWrap.ssl.onhandshakedone (node:internal/tls/wrap:966:12) The root cause: Renovate runs on Node.js, and Node doesn't use the Windows certificate store by default. Our internal Azure DevOps server presents a certificate signed by our internal CA, and Node has no idea that CA exist...

Mixing AddDbContext and AddDbContextFactory: 'Cannot consume scoped service'

Recently I ran into a nasty startup error after registering both AddDbContext and AddDbContextFactory for the same DbContext in an ASP.NET Core project: System.AggregateException: 'Some services are not able to be constructed' System.InvalidOperationException: 'Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory`1[StartStopLijsten.ApiService.Data.StartStopDbContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory`1[StartStopLijsten.ApiService.Data.StartStopDbContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[StartStopLijsten.ApiService.Data.StartStopDbContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory`1[StartStopLijsten.ApiService.Data.StartStopDbContext]'.' The app refuses to start, and although the message is clear why it refuses to start, it doesn't make it obvious how...

Docker snapshot tags aren't as static as I thought

Recently I lost a good chunk of a day chasing an issue in our OWASP Dependency-Track container setup. The strange part: the problem had nothing to do with Dependency-Track itself. It was how I had configured it.   What went wrong I had pinned our docker-compose.yml to a snapshot tag, something like dependencytrack/apiserver:4-snapshot . In my head, a tag like that behaves like a version number: you pull it once, it stays what it is, and you move on. That assumption is wrong. Snapshot tags on Docker Hub are not static. They get overwritten every time a new build lands upstream. The 4-snapshot and 5-snapshot tags on the dependencytrack/apiserver and dependencytrack/frontend repositories are pushed regularly, sometimes daily. So the image I "pinned" last month is not the image I got today, even though the tag in my compose file never changed. Remark: The Dependency-Track docs are actually explicit about this: the latest tag always points to the latest stable ...

Remote control: steering your AI coding session from your phone

There's a moment when you're deep into an agentic coding session and you have to leave your desk (time to catch my train!). Normally that means the session just sits there, waiting, until you're back at your keyboard. Both Claude Code and GitHub Copilot CLI recently shipped a feature that fixes exactly this: you keep the session running locally, but you can check in, approve tool calls, and keep steering it from your phone or any browser. Let's look at how each one works. Claude Code: /remote-control In Claude Code, you enable this with a slash command inside a running session: /remote-control or the short form: /rc You can also start a session already remote-enabled: claude --remote-control or run a dedicated server process that can host multiple concurrent sessions: claude remote-control Whichever way you start it, Claude Code prints a session URL (and lets you press spacebar to show a QR code) that connects to claude.ai/code or the Claude app. ...

Enabling diagnostics in a YAML pipeline in Azure DevOps

When a pipeline run fails and the regular logs don't tell you enough, Azure DevOps has a built-in way to get more detail: system diagnostics. The classic way to turn it on is the "Enable system diagnostics" checkbox you get when you manually queue a run. But if your pipeline is defined in YAML and runs automatically on every push, there's no checkbox to click. The checkbox approach When you queue a pipeline manually, click Run pipeline and you'll see an  Advanced options section with an Enable system diagnostics checkbox. Tick it, hit Run , and that single run gets verbose logging: purple-colored debug lines, extra detail on what each task is doing under the hood. Remark: this only affects the run you're queuing. It doesn't persist, and it's useless for CI-triggered runs where nobody is manually clicking Run. The YAML equivalent: system.debug For YAML pipelines, the checkbox maps directly to a variable: system.debug . Set it to true a...

EF Core savepoints: rolling back part of a transaction

There's a moment when a transaction fails halfway through, and you realize rolling back everything is overkill. You did five inserts, the sixth one violates a constraint, and you'd rather undo just that last operation than start the whole transaction over. That's what savepoints are for, and EF Core has supported them for a while now but until recently I didn't know this feature existed. What's the problem with plain transactions? A regular database transaction is all-or-nothing. Something like this: using var transaction = await context.Database.BeginTransactionAsync(); try { context.Blogs.Add(new Blog { Url = "https://blog1.com" }); await context.SaveChangesAsync(); context.Blogs.Add(new Blog { Url = "https://blog2.com" }); await context.SaveChangesAsync(); // this one fails context.Blogs.Add(new Blog { Url = null }); await context.SaveChangesAsync(); await transaction.CommitAsync(); } catch { a...

Passing custom parameters through OpenIddict's client authorization request

In my passthrough authentication post , the login endpoint set a provider value on AuthenticationProperties so the OpenIddict server would know whether to challenge GitHub or ADFS: app.MapGet("login/{provider}", (string provider) => Results.Challenge(new AuthenticationProperties { RedirectUri = "/", Items = { ["provider"] = provider } }, [OpenIddictClientAspNetCoreDefaults.AuthenticationScheme])); Run that as-is and the server never sees it. Items round-trips through your own app's correlation cookie — it was never meant to become part of the outgoing OAuth2 request. Why this doesn't just work AuthenticationProperties.Items is an ASP.NET Core concept. It's how the authentication middleware remembers things like the redirect URI across the round trip to an external provider and back. The OpenIddict client builds the actual /connect/authorize request from a completely different object — an OpenIddictReq...

Passthrough authentication with OpenIddict

In most enterprise applications authentication is externalized to an external identity provider. For our applications we had to support multiple identity providers. How can we do this without tight coupling to each of these providers? In this post we'll bring OpenIddict into the picture and show how it helps to solve this nicely. What is OpenIddict? OpenIddict is an open-source OAuth2/OpenID Connect server and client stack for .NET. It plugs into ASP.NET Core Identity (or your own user store) and lets you run your own authorization server instead of depending fully on an external one. You get the standard endpoints — /connect/authorize , /connect/token , /connect/userinfo — backed by your own database and your own claims. That's exactly what makes the passthrough pattern possible: OpenIddict is the piece in the middle that can defer to external providers for the actual authentication, then still be the one issuing the token your APIs trust. The naive approach The ...