Skillwright
An Obsidian plugin to run Claude Code skills inline against Ollama, OpenAI, or Anthropic.
A skill is a folder with a SKILL.md in it. Frontmatter for the name and description, markdown body for the instructions. That's not a Claude Code feature, that's a file format — and any tool that reads markdown can use it.
So I built Skillwright, an Obsidian plugin that runs skill folders in the exact Claude Code layout against Ollama, OpenAI, or Anthropic. Highlight a paragraph, pick a skill, get a diff back. It reads ~/.claude/skills straight off disk — same file, same path, not even copied — so my tighten skill works in my notes vault, against a local model, with no API key involved.
The plugin part is dull. Settings tab, modal, three HTTP clients. The interesting part is the gap between what a skill expects and what a chat completion gives you.
A skill assumes an agent, a chat completion isn't one#
A skill doesn't carry all its content — it delegates. Mine say things like "read references/BRAND.md before producing any visual asset." That works in Claude Code because there's an agent on the other end with file tools. A chat completion has no tools and no loop. One request, one response. "Go read BRAND.md" arrives as a sentence the model can do exactly nothing with.
First test run: the skill loaded, the model politely ignored the references it couldn't see, and the output came back generic. Worked on the surface, wrong underneath.
The fix is obvious in hindsight. If the model can't fetch files, the plugin fetches them first — it scans the skill body for references and inlines every one into the system prompt. The skill still says "read BRAND.md," but now BRAND.md is sitting right below that sentence. Fig. 1 shows the whole path from selection to preview.

Reference resolution is the actual product#
Detecting references sounds trivial until you look at how skills actually get written. Links and wikilinks are the easy cases. The one that matters is the bare mention — "follow BRAND.md §5" in prose, no link syntax at all. My own skills do this constantly. Miss the bare form and half the ecosystem quietly degrades.
So the resolver handles all of it, with rules tight enough to trust:
- Paths resolve relative to the file doing the referencing, same as on disk.
- A bare mention carries no directory, so if nothing matches next to the file, the skill folder gets searched by filename.
- Referenced files can reference further, but only two hops from
SKILL.md. Each file inlines once. No cycles, no runaway prompts. - Only
.mdfiles inside the skill's own folder get read. Other file types, escape paths (../../secrets.md), reaches into a neighboring skill, symlinks pointing out — all refused. - Anything that looked like a reference but didn't resolve gets named in a notice.
That own-folder scope is security work, not tidiness. A skill's references decide what gets read on your behalf and shipped to your provider, so a skill you downloaded from a stranger can't go read your notes. (Loose single-file skills fall back to the whole skills folder, so write those yourself.)
There's a budget too — inlined content caps at 40,000 characters, and anything past it gets skipped whole and named in a notice. On an 8B local model with a modest context window, that cap isn't a safety rail. It's load-bearing.
Resolution runs at rewrite time, not when the picker loads — forty skills cost nothing until one runs. Fig. 2 shows where each piece of a skill ends up in the request.

CORS is why every Obsidian AI plugin has the same weird line in it#
Obsidian's UI runs in an Electron renderer, which is Chromium, so plain fetch plays by browser rules and the server decides whether your origin is welcome. OpenAI and Anthropic deliberately say no to browser origins — they don't want API keys living in web pages. Localhost doesn't rescue you either; Ollama checks origins too, so localhost:11434 fails unless you've set OLLAMA_ORIGINS to allow the app.
Obsidian's answer is requestUrl, which fires from Electron's main process instead of the renderer. Node territory. No origin, no CORS, same behavior on mobile. The trade is no streaming — which I'd have fought, except you can't diff a result that's still arriving.
Local models reroll, so the modal plans for it#
I've run the same tighten skill against qwen3, llama3.1:8b, phi4, gpt-oss:20b, and gemma3:4b. qwen3, phi4, and gemma3:4b have been the consistent ones — which surprised me on gemma3, four billion parameters punching well above its weight on rewrite work. llama3.1:8b wanders. Sometimes it tightens the passage, sometimes it hands you a new one with the same topic and different facts.
That shaped the preview modal more than any design instinct did. Results come back as an inline diff with a Diff | Edit toggle for hand-edits. Re-Run reissues the identical request without closing anything and keeps every attempt — ‹ attempt 2 / 3 › arrows flip between them, and Replace / Insert below / Copy act on whichever one is on screen. On a local model, rerolling isn't an edge case. It's the workflow.
Skills are infrastructure now, act like it#
The real finding isn't the plugin. It's that the skill file format survived a second runner without a single file edit, off the same paths Claude Code reads. Frontmatter, body, reference files — all of it still meant something in a tool with a completely different execution model. That's the test a format has to pass before you get to call it infrastructure.
One honest caveat if you go past Ollama: your API key lives in plain text in the plugin's data.json, inside .obsidian/, which your vault sync happily replicates. Obsidian gives plugins no keychain — every plugin that talks to a paid API has this shape. Use scoped, spend-capped keys so a leak is a chore instead of an incident. Or stay local, which is the default for a reason.
It's already changed how I write skills: assume less about the runner. A skill that says "follow BRAND.md" ports. A skill that says "run scripts/check.py and read the output" doesn't — not without a tool loop I haven't built. Portable skills lean on content, not capabilities.
Full source and setup on GitHub: jchimp/skillwright.