dotnet-write
Skill2 starsChanged 53 days agoMITView on GitHub
Use when writing, adding, editing, modifying, refactoring, renaming or deleting C#/.NET code — changing a method or type, changing a signature, adding a class or a file, fixing a compile error or an analyzer warning, or applying review findings. Carries the write path for .cs files (Edit/Write, string-anchored; rename_symbol for a rename), the standards step that runs before the first edit of a session, how the index follows your edits, and what to check before and after a change.
In this repository10 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- dotnet-architectureSkill
- dotnet-consistencySkill
- dotnet-exploreSkill
- dotnet-initSkill
- dotnet-performanceSkill
- dotnet-readSkill
- dotnet-reviewSkill
- dotnet-selfevalSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 22
Resolve the prefix once per session rather than assuming it: Run ToolSearch("select:mcp__plugin_dotnet-toolkit_dotnet__workspace_status").
- Very specific: line 118
The server was unreachable for a new file: call reload_workspace(scope: "all") yourself.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Changing C# with dotnet-toolkit
Edit and Write are the write path for .cs files. An edit is anchored on the exact text you
already read, so there is no line arithmetic to get wrong. The plugin's part of a write is everything
around it: locating the code and its blast radius before, keeping the index in step during, and
rename_symbol for the one edit a string replacement cannot make safely.
Nothing compiles for you as you edit. dotnet build is the gate, run after a burst of related
edits rather than after each one — it reports every dependent break in every project, uncapped, and
TreatWarningsAsErrors repos turn analyzer findings into failures there too.
Tool names carry a host-dependent prefix. In Claude Code they are
mcp__plugin_dotnet-toolkit_dotnet__<tool>, because the plugin qualifies the server it ships. Codex
registers the same server under the bare name dotnet and namespaces by that instead, so the prefix
differs there. Every fully-qualified name below is written in the Claude Code form.
Resolve the prefix once per session rather than assuming it. Run
ToolSearch("select:mcp__plugin_dotnet-toolkit_dotnet__workspace_status"); if that returns nothing,
search for the bare tool name (ToolSearch("workspace_status")) and read the prefix off whatever
comes back, then apply that same prefix to every name in this skill. One probe settles it for the
whole session.
Step 0 — workspace_status before any edit
Call it first, every session, and again before editing after any git operation. It is free and takes no arguments. Three things depend on it:
- A change planned from a
degradedorstaleworkspace is planned from the wrong content. Ifworkspacereports failed projects, fix the build andreload_workspacefirst — aget_referencesanswer from a half-loaded compilation can miss call sites entirely, which is a wrong blast radius, not a thin one. pluginRoot, which is how you reach the coding standards below and any tool manual. Join it yourself:<pluginRoot>/standards/index.md,<pluginRoot>/docs/tools/rename_symbol.md. Never write${CLAUDE_PLUGIN_ROOT}into a path — it is not expanded inside a rule or an agent definition, so it stays literal and the read fails.- Confirmation the index is ready, so
get_referencescan tell you the blast radius rather than returningworkspace_loading. The same line reports when the symbol store last refreshed and whether any files are still queued — see "How the index follows your edits" below.
Step 1 — read the standards, once per session
<pluginRoot>/standards/index.md is the routing table: the baseline set every C# change needs, plus a
row per aspect keyed to an observable property of the code you are about to write. Read the baseline
set plus every row that matches your change, and skim antipatterns.md once per session so a rule
saying "avoid the X antipattern" resolves to something you have actually read.
They are plugin-owned, never copied into a repo, and there is no per-repo override. Nothing auto-loads them — this step is what loads them. Hold them; don't re-read per edit.
Writing to the standard beats fixing to it. dotnet-code-review checks against these same files
afterward.
The write-time checklist
The handful of items most expensive to catch late — credential-shaped literals, concatenated SQL,
unmarked endpoints, in-memory database substitutes in tests that assert relational behavior — arrive
from the hint-cs-write-checklist hook on the session's first Edit/Write of a compiled .cs
file. They are not repeated here: the hook reaches a caller who never loaded this skill, which is
exactly the case a copy here would miss, and two copies always drift.
Step 2 — locate, then read the exact text you will replace
Reading is part of the write path, not a separate task. dotnet-read owns the tools: it is what
turns "the method that handles X" into a symbolId, a file and a line range. From there:
get_symbolgivesdeclarationSites—file,startLine(the line a leading///comment starts on, when there is one),endLine(the closing brace) andsignatureLine— for every part of a partial. Read the text withRead(file, offset: startLine, limit: endLine - startLine + 1).- Anchor the
Editon text from that read, generous enough to be unique in the file. An edit that fails to match means the file moved since you read it: re-read, never guess. - Before changing a signature, a return type or a public contract,
get_references(callers, implementations, overrides) and, for anything virtual or on an interface,get_call_hierarchy. That is the list of files the same change has to reach, and it is the answer grep gets wrong.
rename_symbol — the one edit Edit cannot make
A rename is not a search-and-replace: it has to reach interface, virtual and delegate dispatch sites
and leave alone unrelated text that shares the name. rename_symbol derives every site from Roslyn's
own reference graph, so a pure rename goes here, never through a pile of Edit calls.
ToolSearch("select:mcp__plugin_dotnet-toolkit_dotnet__rename_symbol")apply: false(the default) is a dry run: every file that would change and how many occurrences in each, nothing written. Worth it on a widely-referenced symbol, because the blast radius is the thing you cannot predict.apply: truewrites the files and refreshes the index in the same call, so the new name and itsnewSymbolIdare searchable at once.- The result is not compiled.
nameAlreadyExistswarns when the new name is already declared in the same container — a same-signature clash will not build; a different-signature one silently becomes an overload set. Rundotnet buildafterwards, as after any edit. - It does not rename the containing file; when that is wanted the response says so under
fileRenameHint, and the move is agit mv.
Rename plus other edits: rename first, then Edit the rest against a fresh read.
Manual: <pluginRoot>/docs/tools/rename_symbol.md
How the index follows your edits
Change detection is an mtime poll (inotify does not fire on WSL /mnt/*), and a PostToolUse hook
fires after every Edit, Write, NotebookEdit, apply_patch and Bash call, sending the files
it wrote to the running server. The server re-stats them, swaps the new text into the live Roslyn
solution, and re-indexes just those documents — so search_index, the sym_ ids, get_references
and the hierarchies see your edit before your next call. The hook is silent on success. You will
see a note only when there is something to wait for:
- A new or deleted
.csfile starts a workspace reload, because only a reload can add a document to the compilation. Callworkspace_statusand wait forloadedbeforeget_symbol,search_indexorget_referenceson that file. - A refresh is still running ("queued"):
workspace_statussays how many files are pending. - The server was unreachable for a new file: call
reload_workspace(scope: "all")yourself.
An edit the hook does not recognise — an unusual shell shape, an external editor — converges on the
next sweep within a couple of seconds; limitedBy: "stale" on a read means that sweep has not yet
run, and the next call after it will be current.
The cheap-route table
| Anti-pattern (route taken) | Cheap route |
| Counting lines to place an edit | Anchor Edit on the text itself; a line number is a Read offset, never an edit target |
get_symbol with source just to find the text to change | get_symbol for declarationSites, then one Read of that range — the text you read is the anchor |
| Search-and-replace over the tree to rename something | rename_symbol |
One Edit per call site of a renamed symbol | rename_symbol — every reference derived from the compiler's graph |
grep -n for callers before a signature change | get_references (callers), then get_call_hierarchy for the virtual/interface fan-out grep cannot see |
dotnet build after every single edit | Once per burst of related edits; the build reports every break at once |
reload_workspace after every edit | Nothing — the refresh hook and the sweep already did it; reload only after adding/deleting files when the hook said the server was unreachable, or after a large git operation |
Editing a new .cs file straight after Write | Wait for workspace_status to report loaded — the reload the hook started has to finish before the file is in the compilation |
A pragma suppression to get past an analyzer rule | Fix the code, or raise lowering that rule's .editorconfig severity with the user — never suppress silently |
When it fails
Editreports no match. The file moved since you read it (a previous edit of yours, a rename, a pull). Re-read the range from a freshget_symbol; never retry with a guessed anchor.dotnet buildfails in files you did not touch. That is the dependent breakget_referenceswould have listed: fix every site the build names, in one pass, then build again.- A read answers
limitedBy: "stale"or"degraded". Stale clears on the next sweep; degraded means a project failed to load —workspace_statusnames it, and nothing planned from that compilation should be trusted until the build is fixed andreload_workspacehas run. rename_symbolanswersstale_workspace. The workspace's copy of a file the rename touches is behind disk, usually mid-reload:workspace_status, wait forloaded, retry.- An analyzer rule blocks the build. Fix the code, or lower that rule's severity in
.editorconfigif it is wrong for this repo. The second is a real answer, not a workaround — but it is the user's call, so raise it rather than editing.editorconfigunasked.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

