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.
- Permission gateswrite, shell and network are false in a fresh config, and a tool that declares one it does not hold is refused before its input is parsed.
- Hardened git10 neutralising -c flags on every git call, from the one helper a test enforces, so reading a repository cannot run its code.
- Per-mode tool sets5 modes advertise 8 to 16 of the 40 tools; the whole registry costs 24,323 characters before you have typed anything.
- Context budgetingPrompt, tools and history are counted against a 32,768-character window before the request leaves, and one tool result is capped at 6,000.
- Bounded subagentstask spawns a child with its own window - 8 turns, 30 tool calls, 120 seconds - and no permission the parent does not hold.
- Detachable tasksA job too long for one request is queued on the runs API and rejoined from the last event id after a disconnect.
- Any modelCosmic 1 when you sign in, your own OpenAI or DeepSeek key when you would rather, and a chain between them in the config file.
- Terminal, desktop, APIOne approved account reaches the CLI and any OpenAI SDK pointed at this site's /v1; the macOS app is built but not handed out yet.
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.
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.
// 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);
}| Flag | From a repo config | What the key does |
|---|---|---|
| core.fsmonitor= | Live path | A 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/null | Live path | Hooks 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=cat | Defence in depth | The 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 depth | The 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 depth | The 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 depth | The 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 depth | Credential 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 path | The 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=never | Defence in depth | An 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 depth | A 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.
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.
{
"version": 1,
"provider": "ultrac",
"model": "cosmic-1",
"mode": "default",
"maxSteps": 6,
"permissions": {
"write": false,
"shell": false,
"network": false
}
}write
default falseWriting 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 falseExecuting 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 falseLeaving 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.
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
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.
$ 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, taskWhy 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.
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.
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.
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.
The default. Signed in with ultrac login.
Your own OpenAI key from the environment. OPENAI_BASE_URL aims the same provider at anything that speaks the API.
Your own DeepSeek key from the environment, against api.deepseek.com or a base URL you set.
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.
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 newerThe 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 yetChats 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 onlyThe 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.
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.
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