For Claude Code, Codex, pi and any MCP client

File tools built for how agents work.

Vegvísir replaces grep, cat, sed and an agent’s built-in file tools with a find, read and edit toolbelt made for agents: fewer round trips, edits that cannot land on the wrong lines, errors that say what to do next, and per-agent change tracking when several agents share one repo.

Vegvísir is the Icelandic wayfinder stave, carried so its bearer would not lose their way. This one helps agents find theirs around a codebase.

$npx -y @mortenmo/vegvisir-mcp /path/to/workspace
agent · vegvisir
search({ pattern: "createOrder", and: ["cart"] })
src/api/checkout.ts · in handleCheckout
  35-  const cart = await loadCart(req)
h_src/api/checkout.ts:36:8f92a1b2-1+4
  36:  return createOrder(cart)

edit({ handle: "h_…checkout.ts:36:8f92a1b2-1+4",
  edits: [{ oldText: "createOrder(cart)",
    newText: "createOrder(cart, key)" }] })
✓ 1 replacement
  h_src/api/checkout.ts:35-40:c41d07e9

edit({ handle: "h_…checkout.ts:1-50:0be5…" })
STALE the file changed since this handle.
  Use the handle your last edit returned:
  h_src/api/checkout.ts:35-40:c41d07e9

Why it exists

Agents spend too much of their time on file plumbing.

Shell tools were made for people at a prompt. An agent uses them through a narrow straw, one call at a time, and pays for every mistake in turns and context.

Agents waste turns on file plumbing

They grep, then cat a range, then cat again because the range was wrong, then sed, then re-read to check that the sed worked. Every round trip costs time and context.

Text edits go wrong silently

An edit hits the wrong occurrence, the file changed since it was read, or line numbers copied from output end up inside the code. Nothing complains until the build does.

Several agents trip over each other

With more than one agent in a repo, nobody can tell which agent changed what, and committing means untangling it by hand.

Before and after

The same change, in half the turns.

Passing an extra argument to one call site. On the left, the loop every agent knows. On the right, the same change through Vegvísir.

With grep, cat and sed · 6 turns

  1. grep -rn createOrder src12 hits, half of them in tests
  2. cat -n src/api/checkout.ts | sed -n 20,40pthe range missed the call
  3. sed -n 30,60p src/api/checkout.tsfound it, on line 36
  4. sed -i 's/createOrder(cart)/…/' src/api/checkout.tsalso matched a second call on line 81
  5. cat -n src/api/checkout.tsre-read to see what the sed did
  6. sed -i '81s/…/createOrder(cart)/' …put the second one back by hand

With Vegvísir · 2 turns

  1. search("createOrder", and: ["cart"], not: ["test"])one hit, labeled handleCheckout, with its handle
  2. edit({ handle, edits: [{ oldText, newText }] })scoped to line 36, so line 81 cannot match; returns a fresh handle and needs no re-read

Core ideas

A handle is proof the agent looked.

Every read, search and definition returns a handle that packs the path, the line range and a hash of the content into one string. Edits take that handle and nothing else.

h_src/auth.ts:path1-50:line range8f92a1b2content hash

Observe before you edit

Every edit needs a handle from an earlier read, search or definition. An edit without one is refused with NOT_READ, so an agent cannot change a file it has not looked at.

Handles are capabilities

An edit is scoped to its handle’s range, plus two lines of grace, and fails STALE if the file changed underneath. Each edit returns a fresh handle, and handles on other regions of the same file stay valid because Vegvísir shifts their line numbers.

Errors say what to do next

Every refusal names what happened and the concrete next call, and retryable errors are told apart from ones that are not.

Output sized for a model

Every line carries its line number, like grep -n and cat -n, so agents cite real lines. Numbers copied into an edit are stripped and the indentation kept. Metadata a model does not act on is left out.

STALERe-read this range, or use the handle your last edit returned.
AMBIGUOUSHere are the candidate sites, each with its own handle.
NOT_READRead the file first, then edit with that handle.
READ_ONLYThis path cannot be written. Do not retry.

The tools

Eleven tools, each doing one job well.

Find with search, definition, references and outline. Read with read and ls. Change with edit, editMatches and write. Then, from the next release, review and commit your own work with changes and commit.

search

Literal by default, regex when asked. One call does what a pipeline of greps would.

  • and (several strings on one line) and not (exclusions), with 1 line of context before and 4 after by default.
  • Each hit is labeled with its enclosing function or class, and is a handle that reads like a compiler location.
  • Modes for spans, files, counts and matches; within narrows an earlier result without losing its handles.
  • Skips node_modules, dist, .git, binaries and huge files. Lockfile hits are counted, not listed. A .vegvisirignore tunes the rest.
search
search({ pattern: "token", not: ["test"], mode: "files" })
3 files · +2 in pnpm-lock.yaml
src/auth/session.ts   4 hits
src/auth/refresh.ts   2 hits
src/api/client.ts     1 hit

definition

The whole body of a function or class, by name, Class.method, or a search hit’s handle.

  • A name defined more than once comes back AMBIGUOUS with a handle per definition.
definition
definition("Session.refresh")
h_src/auth/session.ts:42-58:5a10ce3f
  42	  async refresh(): Promise<void> {
  43	    const next = await this.client.renew(this.token)
  …

references

Every use of a name, grouped by kind: definition, import, call, JSX, type, member access, object key and more.

  • Matches in comments, strings and docs are counted, not listed.
  • Hits are editable sites, so a rename is references followed by editMatches.
  • Syntactic, not type-resolved: x.start on an unknown receiver is reported as member access.
references
references("loadCart")
definition   1  src/cart/load.ts:12
import       3  src/api/checkout.ts:4 …
call         5  src/api/checkout.ts:35 …
+2 in comments and docs

outline

A file’s shape without its bodies: imports, then declarations nested under their containers, with signatures and the first line of each doc comment.

  • Reach for it before reading a large file.
outline
outline("src/auth/session.ts")
imports      1-8
class Session                    12-96
  constructor(client, token)     14-20
  refresh(): Promise<void>       42-58  Renews before expiry.
  revoke(): Promise<void>        60-71

read

Line-numbered, with offset and limit, and capped at 2,000 lines, 2,000 characters a line and 50 KB, so one read cannot flood the context.

read
read("src/cart/load.ts", { offset: 10, limit: 3 })
h_src/cart/load.ts:10-12:77e0b4d2
  10	import { db } from '../db'
  11
  12	export async function loadCart(req) {

ls

Directory entries with size, kind, modification time, permissions and symlink targets.

ls
ls("src/auth")
session.ts   file  3.1 KB  rw  2m ago
refresh.ts   file  1.2 KB  rw  1h ago
keys         link  → ../../keys

edit

Exact oldText → newText replacements inside a handle’s scope, several in one call as long as they do not overlap.

  • Narrow fuzzy matching absorbs smart quotes, dashes and trailing whitespace; a guard rejects matches far larger than the text asked for.
  • CRLF line endings and byte-order marks are preserved.
edit
edit({ handle: "h_src/cart/load.ts:10-12:77e0b4d2",
  edits: [{ oldText: "loadCart(req)",
            newText: "loadCart(req, opts)" }] })
✓ 1 replacement · h_src/cart/load.ts:10-12:3c9f11a0

editMatches

One replacement across chosen search hits, all or nothing. Only hits from a search can be swept, so an agent enumerates before it changes anything.

editMatches
editMatches({ handles: ["s_4e…#1", "s_4e…#3"],
  newText: "loadBasket" })
✓ 2 sites in 2 files

write

Create a file, or overwrite an existing one by its handle.

write
write({ path: "src/cart/opts.ts", content: "…" })
✓ created · h_src/cart/opts.ts:1-14:d02a6e51

changesNext release

This agent’s own uncommitted hunks, each with the lines it covers now and an edit handle, so an agent can review its work before it says it is done.

  • Flags lines another agent has since overwritten.
  • Flags lines where this agent built on someone else’s uncommitted work.
  • Flags files changed outside Vegvísir: a shell, an IDE, you.
changes
changes()
src/api/checkout.ts   +1 -1  h_…:36:91ab…
src/cart/load.ts      +3 -1  h_…:12-14:3c9f…
! src/cart/load.ts:13 since changed by agent codex-2

commitNext release

A git commit of exactly this agent’s lines. Everything else stays uncommitted in the working tree, even in the same files.

  • Real git commit, so hooks, signing and author config apply; core.autocrlf and .gitattributes are respected.
  • Refuses with a list of the problems when lines are genuinely tangled.
commit
commit({ message: "Pass an idempotency key to checkout" })
✓ 3f1c9a2 · 2 files · 4 lines
left uncommitted: 3 hunks by claude-1, 1 by you

Language support. A zero-dependency heuristic parser covers every language. The optional tree-sitter layer, @mortenmo/vegvisir-syntax with its grammars bundled as WebAssembly, gives exact declarations for TypeScript, TSX, JavaScript, Rust, Python, Java, Kotlin and Swift. The MCP server and the pi extension use it automatically.

Next release · per-agent change tracking

Several agents, one repo, and every line accounted for.

Every edit an agent makes through Vegvísir is recorded under that agent’s id. Vegvísir can tell apart several agents’ uncommitted edits, and yours, even in the same file and on neighboring lines, and commit exactly one agent’s lines while the rest stay in the working tree. It ships in the release after 0.3.0.

weave · src/cart/load.ts
baseexport async function loadCart(req) {
claude-1  const rows = await db.cart(req.user)
claude-1  const rows = await db.cart(req.user, opts)
codex-2  if (!rows.length) return EMPTY_CART
base  return toCart(rows)
you  // TODO: cache this
base}
claude-1codex-2you, outside Vegvísirstruck removed by the agent shown
commit as claude-1

Takes the changed db.cart call. codex-2’s guard and your TODO stay uncommitted.

commit as codex-2

Takes only the empty-cart guard, applied onto HEAD as its own commit.

changes for claude-1

Lists its one hunk with a handle, and notes that it sits next to work by codex-2.

  1. Each agent keeps a journalEdits are appended to the agent’s own journal in .git/vegvisir/, with file versions in git’s object store.
  2. Edits are wovenVegvísir replays every agent’s edits to a file into a weave, where each line knows who added it and who removed it.
  3. One agent is projectedProjecting the weave gives the file with only that agent’s changes.
  4. Commits merge at line levelThe commit is built in a temporary index and merged three ways onto HEAD, then made with real git.

Built for real concurrency

Writes are serialized across agent processes, so concurrent edits never lose each other. With 8 agent processes editing one file at once, every edit landed, each was attributed to the right agent, and every commit came out exact.

Tested against chaos

Randomized tests, including a soak run of 32 longer sessions, put agents and a simulated user editing the same files together.

For harnesses and UIs

A CLI (vegvisir changes --all --json, commit, agents, prune, forget, adopt) and a documented JSONL format. No model is involved in any of it.

An honest limit. A commit holds exactly one agent’s lines, but those lines can depend on someone else’s uncommitted code. Whether the commit builds on its own is still yours to check.

Works with

Any MCP client, and at home in Ordne.

Vegvísir ships as an MCP server over stdio, an extension for the pi coding agent, and an embeddable engine with zero runtime dependencies for Node 20 or later and Bun.

Claude CodeCodexOpenCodepiAny MCP client

One setting per project

Turn Vegvísir on for a project in Ordne and its agents get the tools automatically. A settings override can point at a local build.

Agents steered to use it

Ordne’s agent instructions tell agents to reach for Vegvísir rather than shell file commands, so the habit sticks across runtimes.

Changes, per agent

With change tracking, Ordne can show which agent changed which lines and offer a commit per agent, keeping history tidy without anyone sorting hunks by hand.

Process output, as files

An agent’s process output is mounted read-only at /output, so dev server logs are searchable with the same tools as code.

Get started

Install it in a minute.

MCP: Claude Code, Codex, OpenCode and others

terminal
npx -y @mortenmo/vegvisir-mcp /path/to/workspace

Or add it to your client’s MCP configuration. VEGVISIR_AGENT_ID is optional; give each agent a stable id for change tracking.

mcp.json
{
  "mcpServers": {
    "vegvisir": {
      "command": "npx",
      "args": ["-y", "@mortenmo/vegvisir-mcp", "/path/to/workspace"],
      "env": { "VEGVISIR_AGENT_ID": "my-agent" }
    }
  }
}

pi

terminal
pi install npm:@mortenmo/vegvisir-pi

As a library

engine.ts
import { VegvisirEngine } from '@mortenmo/vegvisir-core'
import { TreeSitterSyntax } from '@mortenmo/vegvisir-syntax'

const engine = new VegvisirEngine({
  rootDir: process.cwd(),
  syntax: new TreeSitterSyntax(),
  changeTracking: true,
})

FAQ

Questions worth asking.

Is it safe to give an agent?

Paths outside the workspace are unreachable, symlink escapes included. Every edit is a compare-and-swap on a content hash, so it fails rather than landing on text the agent never saw. Writes to one file are serialized across processes, and editMatches is all or nothing.

Does it replace git?

No. Change tracking sits beside git and commits through it, with real git commit, so your hooks, signing and history are untouched. Its journal lives in .git/vegvisir/ and its file versions in git’s own object store.

Which languages does it understand?

Every text file, through a zero-dependency heuristic parser. With the optional tree-sitter layer, which the MCP server and the pi extension load automatically, declarations are exact for TypeScript, TSX, JavaScript, Rust, Python, Java, Kotlin and Swift.

Will it slow my agent down?

It is meant to do the opposite: the same work in fewer round trips. Searches skip dependency and build folders, binaries and very large files, and every output is capped so one call cannot flood the context.

Does it need Ordne?

No. Vegvísir is a standalone set of npm packages that works with any MCP client. Ordne is where it is most at home, because Ordne can show each agent’s changes and commit them per agent.

Can my agent still use the shell?

Yes. Vegvísir replaces file plumbing, not the shell. The pi extension can additionally block read-only shell file commands such as cat and grep, and point the agent at the matching tool instead; set VEGVISIR_BASH_GATE=0 to turn that off.

Eight staves pointing every way, so an agent always knows where it is in your code.