Skip to content

Codemode

Sriinnu edited this page Oct 6, 2026 · 3 revisions

Codemode

An agent exploring a repo pays for every intermediate result: each read and each grep is a round-trip whose raw output lands in the transcript and is sent again on every later turn. With omit codemode the model writes a script, the reads happen inside it, and only what the script returns comes back.

Experimental, and read-only.

Setup

Codemode ships in omit 0.5.0 and later.

npm install -g @sriinnu/omit @earendil-works/pi-codemode

@earendil-works/pi-codemode is the sandbox, and it is an optional peer: omit installs without it and every other command works without it. Codemode needs Node 22.19 or newer.

With Homebrew there is nothing extra to install: the formula brings the sandbox with it.

brew install sriinnu/tap/omit

Any agent with a shell

Nothing has to be registered. omit codemode run takes a script on stdin, or from a file, and prints the answer:

omit codemode run <<'EOF'
return (await tools.grep({ pattern: 'TODO' })).length
EOF

Quote the delimiter ('EOF') so the shell leaves the script alone. Exit status 1 means the script failed or its result was withheld.

The omit-codemode skill teaches an agent when to reach for this and what a script can call. It is skills/omit-codemode/SKILL.md in the repo: copy it into your agent's skills directory, for example .claude/skills/omit-codemode/SKILL.md.

As an MCP server

For a host without a shell, or one where you would rather approve a single read-only tool:

claude mcp add omit -- omit codemode        # Claude Code
# Codex: ~/.codex/config.toml
[mcp_servers.omit]
command = "omit"
args = ["codemode"]

The directory the server starts in is the workspace: every path is relative to it and confined to it.

Writing a script

A script is the body of an async function, so await and return work at the top level.

const libs = (await tools.files({ under: 'lib' })).filter((f) => f.endsWith('.mjs'))
const sources = await Promise.all(libs.map((path) => tools.read({ path })))
return Object.fromEntries(libs.map((f, i) => [f, (sources[i].match(/^export /gm) ?? []).length]))
Call Resolves to Notes
tools.files({ under? }) string[] Tracked and untracked files, minus what git ignores.
tools.read({ path }) string One file as text, up to 8 MiB. Past that, use grep.
tools.grep({ pattern, under? }) { path, line, text }[] Extended regular expression. No match is an empty array.

text(value) and console.log add to the output; return adds the returned value. A call that fails rejects with an Error, so use Promise.allSettled to keep the results of the calls that worked.

There is nothing else in there: no file system, no network, no process, no timers.

What comes back

  • Whatever the script printed, then what it returned (non-strings as JSON).
  • Past 20,000 characters the middle is cut, and the result says how much. A script that returns that much has not filtered anything: count, aggregate or filter instead.
  • A result that matches a secret rule is withheld whole. A script may read a credentials file; it may not carry the key out.
  • On failure, the partial output followed by Script error (<kind>): …. A script has 60 seconds.

Limits

  • Read-only. There are no write or edit tools yet.
  • files and grep need a git repository. They read the tree through git, which is what knows what is ignored. read works anywhere.
  • One workspace per server. Paths cannot leave it: no .., no absolute path, no symlink pointing out.

Clone this wiki locally