Ultrac
ultracode

A closed-source coding agent for repositories you do not trust.

Cosmic 1 the moment you sign in, or your own OpenAI or DeepSeek key, or any host that speaks that API: the harness is the product and the model is a setting. The harness itself ships as a build rather than a repository, because what stands between an untrusted checkout and your machine should be a gate you can watch refuse from your own terminal.

provider ultrac · model cosmic-1 · mode review · native toolsworkspace /Users/dev/acme-apitools 8 (mode review) ≈ 5,452 chars · window 8,192 tokens · tool results capped at 6,000 chars
acme-api - ultracode
>Review the session TTL change and tell me what to run.
Reading the diff before I say anything about it.
git_diff {"staged":false}
diff --git a/src/session.ts b/src/session.ts
@@ -1 +1 @@
-export const SESSION_TTL_SECONDS = 3600;
+export const SESSION_TTL_SECONDS = 900;
verification_plan {}
Files to account for:
- M src/session.ts
Recommended checks:
- run_package_script {"script":"typecheck"} (typed source or config changed; terminal: npm run typecheck)
- run_package_script {"script":"test"} (behavior coverage for source changes; terminal: npm run test)
run_shell wants shell access. [o]nce / [a]lways / [n]o › n
✗ denied shell for run_shell
verification_report {"outcome":"unknown"}
# Verification Report
Generated: 2026-08-23T02:53:03.218Z
Outcome: unknown
HEAD: cb15885 Add session helper
Two checks are outstanding and I ran neither: shell was denied. The report says unknown, not pass.
Session saved: /Users/dev/acme-api/.ultrac/sessions/2026-08-23T02-53-07-118Z-3f9c1a20.json

Tool names, tool inputs, tool output, the permission prompt, the denial and the saved path are captured from a real run against a scratch repository. The two model replies are illustrative: the model was not run. The session file at the end is written by the harness itself on the way out, not by a tool, which is why it lands in a session that was granted nothing.

Untrusted repositories

A repository can make git run its code. Not this git.

Git is configurable, and a good deal of that configuration names a program for git to execute. Clone a repository, ask an agent to read the diff, and on plain git you have run whatever the author of that repository chose.

Every git invocation in the harness goes through one helper that prepends ten -c flags. A command-line -c beats both repository and global config, so nothing the checkout declares can put any of them back, and a test walks the source for a second place that spawns git and fails the build if it finds one.

That is why the five read-only git tools declare no permissions at all. Once the execution paths are closed, reading history is genuinely read-only, so an agent holding nothing can still report the branch, the log and the commit it is being asked about. The two that change something still declare it: git_commit on write, git_push on network.

every git call goes through this
// src/tools/workspace/git-process.ts
export const HARDENED_GIT_CONFIG = [
  "-c", "core.fsmonitor=",
  "-c", "core.hooksPath=/dev/null",
  "-c", "core.pager=cat",
  "-c", "core.askPass=",
  "-c", "core.sshCommand=",
  "-c", "core.gitProxy=",
  "-c", "credential.helper=",
  "-c", "diff.external=",
  "-c", "protocol.ext.allow=never",
  "-c", "uploadpack.packObjectsHook=",
];

// git reads an empty diff.external as a command and dies
// trying to run it, so the -c above is not enough on its
// own. --no-ext-diff is git's own way to say the key does
// not apply: the repo's driver never runs, and the honest
// diff still renders. Only these subcommands take it -
// `git status --no-ext-diff` is an error.
const DIFF_SUBCOMMANDS = new Set([
  "diff", "log", "show", "whatchanged", "format-patch", "stash",
]);

/** Spawn git with repo-controlled execution disabled. */
export function runGit(args: string[], options: RunProcessOptions) {
  const hardened = DIFF_SUBCOMMANDS.has(args[0] ?? "")
    ? [args[0], "--no-ext-diff", ...args.slice(1)]
    : args;

  return runProcess("git", [...HARDENED_GIT_CONFIG, ...hardened], options);
}
Each hardened git config flag, whether a repository can reach it from its own config today, and what it does.
FlagFrom a repo configWhat the key does
core.fsmonitor=Live pathA repository can name a program git runs to find out which files changed. Left alone, git status executes it. Emptied, status stays a read.
core.hooksPath=/dev/nullLive pathHooks are scripts git runs around commits and checkouts, and a repository can point that lookup at a directory it ships. Aimed at /dev/null, there are no hooks to find.
core.pager=catDefence in depthThe pager is a command git pipes its output into, and a repository can set it. It never fires here, because git only spawns a pager on a terminal and the harness always pipes. Forced to cat regardless.
core.askPass=Defence in depthThe program git runs to ask for a password. A repository can set it, but it only fires when a remote asks for credentials, which the read-only tools never trigger.
core.sshCommand=Defence in depthThe ssh binary git shells out to for a remote. Repository-settable, but remote-only. Emptied so a checkout cannot substitute its own transport on the calls that do reach a remote.
core.gitProxy=Defence in depthThe command git runs to open a connection for a git:// host. Another repository-chosen program, and another one that needs a remote operation to fire.
credential.helper=Defence in depthCredential helpers are commands git runs to fetch secrets. A repository can name one, but it runs only when a remote asks for authentication.
diff.external=Live pathThe strongest one. A repository that sets diff.external turns git diff into run this program over every changed file. Reading a diff becomes execution.
protocol.ext.allow=neverDefence in depthAn ext:: remote URL names an arbitrary command as the transport. It only fires once git opens a transport, and the only tool that does is git_push, which declares network. never refuses the whole protocol rather than trusting the URL.
uploadpack.packObjectsHook=Defence in depthA command git runs in place of pack-objects while serving objects out. Git already refuses this one from a repository's own config - it is honoured only in protected scopes - so emptying it changes nothing today and keeps changing nothing if that ever loosens.

Verified against git 2.50.1: core.fsmonitor runs on git status, and diff.external runs on git diff. Both are ordinary reads an agent performs unprompted.

Permissions

A gate, not a prompt.

Write, shell and network are false in a fresh .ultrac/config.json. A tool that declares one and does not hold it is refused by the registry before its input is even parsed, so a permission cannot be talked out of the agent - only granted by you.

what ultrac init writes
{
  "version": 1,
  "provider": "ultrac",
  "model": "cosmic-1",
  "mode": "default",
  "maxSteps": 6,
  "permissions": {
    "write": false,
    "shell": false,
    "network": false
  }
}

write

default false

Writing a file, applying a patch, committing, or storing a memory. No tool writes a file without it.

write_file · replace_in_file · multi_edit · apply_patch · git_commit · memory_write · memory_delete

shell

default false

Executing anything at all: a shell command, a package script, the test suite, the type checker.

run_shell · run_package_script · run_tests · diagnostics

network

default false

Leaving the machine. Fetching a URL, or pushing a branch to a remote. These two, and nothing else.

git_push · fetch_url

Advertising is not authorization

The tools a mode advertises are what the request carries, not what the agent may do. All 40 stay registered and callable by name, so a model that has read a tool name somewhere can still invoke it - and still meets the same gate.

23 of the 40 declare nothing at all, because reading a file, mapping a repository, planning the checks and reading the diff are things a harness with no permissions can honestly do. 4 more - ast_search, context_bundle, verification_report, task - declare one they use when it is granted and do without when it is not: ast_search runs the ast-grep binary with shell, answers from a pure-Node text search without it, and says which backend it used either way. An optional permission never decides whether a call runs. Only a required one does.

Modes

Context is the scarce resource. Spend it on the task.

The serialised tool array goes out with every single request. All 40 of them cost 24,323 characters, past the 19,660-character prompt limit before a system prompt, before the workspace context, before the first word from you.

Tool payload against the window

8,192 tokens · 32,768 chars

plan8 tools · 4,479 chars · 13.7%
review8 tools · 5,452 chars · 16.6%
default12 tools · 6,292 chars · 19.2%
security14 tools · 7,482 chars · 22.8%
coding16 tools · 10,175 chars · 31.1%
--tools all40 tools · 24,323 chars · 74.2%

The hairline sits at 19,660 characters, the prompt limit the budget enforces. Past it the oldest turns start being dropped. The whole registry crosses it on its own.

plan

Read, search, map, and write the steps down. No editor and no shell, so the mode's promise and its tool list agree.

review

The worktree, its history, the type checker, and the two verification tools it writes its verdict through. Nothing here edits the tree.

default

The compact working set: read, search, run, edit, patch, see what changed, keep a plan. Enough for an ordinary turn end to end.

security

The core plus the two things a security pass needs that a coding pass does not: fetching a referenced advisory, and the project manifest.

coding

The core plus the verification loop - tests, diagnostics, package scripts - and structural search to land an edit in the right place, degrading to text when shell is withheld.

--tools all

The escape hatch, and the reason the modes exist: the whole registry is already over the prompt limit before the conversation starts.

count it yourself
$ ultrac tools --budget
default   12 tools    6,292 chars
  list_files, read_file, search_files, write_file, replace_in_file, multi_edit, apply_patch, git_status, git_diff, run_shell, todo_write, todo_read
plan       8 tools    4,479 chars
  list_files, read_file, search_files, repo_map, git_diff, git_log, todo_write, todo_read
security  14 tools    7,482 chars
  list_files, read_file, search_files, inspect_project, write_file, replace_in_file, multi_edit, apply_patch, git_status, git_diff, fetch_url, run_shell, todo_write, todo_read
coding    16 tools   10,175 chars
  list_files, read_file, search_files, ast_search, write_file, replace_in_file, multi_edit, apply_patch, git_status, git_diff, run_package_script, run_tests, diagnostics, run_shell, todo_write, todo_read
review     8 tools    5,452 chars
  list_files, read_file, search_files, verification_plan, verification_report, git_diff, git_log, diagnostics
all       40 tools   24,323 chars
  list_files, read_file, read_many_files, search_files, ast_search, inspect_project, repo_map, repo_handbook, context_bundle, verification_plan, verification_report, write_file, replace_in_file, multi_edit, apply_patch, git_status, git_worktree_summary, git_diff, git_log, git_show, git_commit, git_push, run_package_script, run_tests, diagnostics, list_sessions, read_session, search_sessions, fetch_url, run_shell, memory_write, memory_read, memory_list, memory_search, memory_delete, skill, list_skills, todo_write, todo_read, task

Why the window is 8,192 tokens

A harness that assumes a large window never has to think about any of this. This one assumes 8,192 tokens, which is 32,768 characters at the four-characters-per-token estimate it budgets in. Fitting the work into that is what makes the agent usable on a quantised model on hardware you own.

Read-only modes advertise nothing that mutates the workspace, so a mode's stated contract and its tool list agree with each other. --tools all and --tools a,b,c override the mode when you want the whole registry back.

Fitting

It knows when it will not fit.

A request that overruns the window comes back as a 400, not as a shorter answer. So the harness measures what it is about to send before the request leaves.

The budget, measured before every request

System prompt plus serialised tools plus history, counted in characters against the window. Over the 19,660-character prompt limit the oldest turns are dropped, keeping the remaining 40 percent as room for the reply. An assistant turn that requested tools and the results that answered it are one unit and go together, because half of that pair is invalid on the wire.

The system prompt and the turn in flight are never dropped. If it still does not fit, the harness refuses locally and prints the arithmetic - system, tools, history, total, window - instead of sending a request it knows will fail.

One tool result cannot eat the session

A single read of a large file, or one search across a big repository, can be larger than the whole window - and because the transcript is replayed on every later turn, that cost is paid again every turn. So a result is capped at 6,000 characters, cut on a line boundary, with a marker saying how much was removed and how to get the rest.

The tool still ran and its full output is still in the saved session. Only what the model is asked to carry shrinks. read_file returns continuable line ranges for the same reason.

Long work

Work that outlives the request.

Two different problems: keeping a search-heavy investigation out of the main context, and keeping a job alive after the terminal that started it has gone.

Subagents, bounded three ways

The task tool spawns a child agent with its own fresh window. The parent spends a task description and gets back a summary instead of the child's whole transcript. A child can never hold a permission the parent does not hold, defaults to read-only, and cannot spawn a child of its own.

Exceeding a bound is not an exception. The run ends with a stop reason and a partial summary, so a parent gets an answer it can act on rather than a hang.

Model turns
8 by default, 24 maximum
Tool calls
30 by default, 120 maximum
Wall clock
120s by default, 600s maximum
Summary handed back
4,000 characters
Depth
1: a child never spawns a child

Tasks, on the runs API

A completion has to finish inside one request. A job that takes minutes cannot, so ultracode queues it as a run on the public API and follows the log over server-sent events. Disconnecting costs nothing: the run keeps going, and the CLI rejoins from the last event id it saw rather than from the beginning.

$ ultrac task "Port the auth middleware to the new session store"
Run 9f2c8b41-6d0e-4a77-9a31-2c5b7e10d4af queued. Following; Ctrl-C cancels it.
[tool_call read_file {"path":"src/auth/middleware.ts"}]
[reconnecting in 1s: terminated (other side closed)]

$ ultrac tasks              # the account's runs, newest first
$ ultrac tasks follow <id>  # rejoin from the last event id
$ ultrac tasks cancel <id>

The endpoints behind those commands, the run statuses, the event types and the reconnect semantics are documented in full under Long-running tasks.

Providers

Your model, or ours.

The harness is the product; the model is a choice. Cosmic 1 is what it signs in to by default, your own keys are one flag, and a chain of providers is a block in the config file.

ultraccosmic-1

The default. Signed in with ultrac login.

openaigpt-5.6

Your own OpenAI key from the environment. OPENAI_BASE_URL aims the same provider at anything that speaks the API.

deepseekdeepseek-v4-flash

Your own DeepSeek key from the environment, against api.deepseek.com or a base URL you set.

echoecho

A local stub that answers without a network call. For trying the loop offline.

What a fallback chain will and will not do

Only transport failures fail over: a network error, a timeout, a 408, a 429, a 5xx. Bad credentials and malformed requests are surfaced unchanged, because quietly answering with a different model would hide your configuration bug from you. Anything the chain cannot classify is surfaced too - failing over is opt-in per error kind, never the default.

Every step that changes which model is answering is announced, and a stream never swaps models once tokens are on the wire. Native tool calling is used where the provider has it, and a text JSON protocol stands in where it does not, so a chain led by a native-tools provider keeps working when that provider is down.

Surfaces

A terminal, a desktop app, and an API.

The harness runs in your terminal, and that is the surface you can have today. The OpenAI-compatible API on this site takes any client you point at it. The macOS app is built and works, and we have not started handing it out. There is no editor extension.

ultracode

Terminal · Node 22 or newer

The harness itself, and the only surface that touches your repository. ultrac login checks a pasted key against GET /v1/models and refuses to store one the API rejects, so a revoked or mistyped key fails at sign-in rather than mid-session.

  • An interactive session, or ask for one reply with no tools and no workspace context
  • tools run <name> calls a single tool and prints exactly what it returned
  • Sessions, memories and skills are files under .ultrac, readable without the agent

Ultrac for macOS

Electron · Apple Silicon · not handed out yet

Chats and tasks in a window, signed in with the same six-digit email code as the website. The session cookie stays in the main process, written with Keychain-backed storage, and never crosses into the page. It builds for Apple Silicon only, it is neither signed nor notarised, and no build is being handed out yet.

  • Chats stream, list and delete against the same API the dashboard uses
  • A task's event log resumes with Last-Event-ID: what it missed, and nothing twice
  • Attach a folder and it hands you the ultracode commands aimed at it; it does not run the agent

/v1

OpenAI-compatible · key only

The endpoints the CLI signs in against are the public ones. A key authenticates and a cookie never does, so a browser session cannot be turned into API access.

  • POST /v1/chat/completions, streaming and native tool calls
  • GET /v1/models, the cheapest authenticated call there is
  • POST /v1/runs, with /v1/runs/:id, its /events stream and /cancel

The CLI authenticates with a key from your keys page; the API takes the same key. Both are open to an approved account today. The macOS app builds for Apple Silicon and is neither signed nor notarised, so there is nothing here to hand you yet - ask and we will tell you where it stands.

The registry

40 tools, and what each one costs you.

Grouped by what they are for. The tag beside a name is the permission it declares, and that is the only thing deciding whether a call runs.

Read the tree

Listing, reading and searching, all scoped inside the workspace. read_file returns a line range you can continue from, so a large file arrives in pieces instead of eating the window.

  • list_files
  • read_file
  • read_many_files
  • search_files
  • ast_searchshell opt

Understand the project

The orientation pass: what this repository is, where its product areas live, and one portable bundle of that context to hand to something else.

  • inspect_project
  • repo_map
  • repo_handbook
  • context_bundlewrite opt

Change files

Four ways to write, from a whole file to an exact replacement to a unified diff. Every one of them declares write, so every one of them is refused until you grant it.

  • write_filewrite
  • replace_in_filewrite
  • multi_editwrite
  • apply_patchwrite

Git, without a shell

Reading history is a separate capability from running commands, so these do not declare shell. They spawn git directly, through the hardened invocation described above.

  • git_status
  • git_worktree_summary
  • git_diff
  • git_log
  • git_show
  • git_commitwrite
  • git_pushnetwork

Run and verify

Executing anything needs shell. Planning the checks and writing the report do not, so an agent with no permissions at all can still say what should have been run.

  • run_shellshell
  • run_package_scriptshell
  • run_testsshell
  • diagnosticsshell
  • verification_plan
  • verification_reportwrite opt

Carry work across turns

A task list for the turn in flight and file-backed memories for everything longer than one. Reading a memory is free; writing or deleting one is a file write.

  • todo_write
  • todo_read
  • memory_writewrite
  • memory_read
  • memory_list
  • memory_search
  • memory_deletewrite

Recall earlier sessions

Every session is saved as a transcript under .ultrac/sessions. These three are how a later run finds out what an earlier one already tried.

  • list_sessions
  • read_session
  • search_sessions

Delegate and extend

A bounded child agent with its own window, SKILL.md playbooks loaded on demand, and the one tool that fetches anything from the open web.

  • taskwrite opt
  • skill
  • list_skills
  • fetch_urlnetwork

Skills are SKILL.md playbooks loaded on demand. Memories are files. Sessions are transcripts on disk you can list, read and search from a later run. Nothing here needs a service to be running for the agent to know what it did yesterday.

Invite only

There is no install command yet, and we are not going to invent one.

ultracode ships from a private repository to accounts we have approved, and the name on npm belongs to an unrelated package, so anything you install from there is not this. Ask for access and we will send you the build and a key.

ultrac · signs in with ultrac login · runs on Cosmic 1 or your own keys