Workspaces

A throwaway clone of one git repository for one run: set up by the repository itself, changed through the workspace tools, pushed to a branch, and deleted when the run ends.

What a workspace is

A workspace is a clone of one git repository, made for one run and deleted when that run ends. A member reads, changes, tests and commits code in it, and pushes the result to a branch. Nothing in it outlives the run except what was pushed.

A run gets a workspace only when its task declares workspace tools: workspace.read_file, workspace.edit, workspace.run_command, workspace.git_push and the rest, each listed by name like any other tool. A task without them never clones anything. What each tool takes and returns is in the workspace tools reference.

Everything a workspace does is plain git and the repository's own tooling: it clones, branches, commits, merges and pushes through the repository's remote, and sets up and checks the code with the commands the repository declares (see What the repository provides). None of that depends on where the repository is hosted: the remote's address says which host it is, and the credential for it comes from the code host tool you bind. One thing still reaches a code host's API of its own: a branch named after an issue. See Where the repository lives and The branch.

The workspace tools only touch the clone. Issues, pull requests, reviews and merging happen on your code host, through the host's own MCP server, bound like any other tool: GitHub has a setup guide, and any other host's server can be bound from Tools.

Where the repository lives

A run names its repository by its git remote address, on any host that serves git: GitLab, Bitbucket, Azure Repos, a self-hosted server, or GitHub.

{ "repo": "https://git.example.com/acme/website.git" }

Nothing about the host is set in the OS. The workspace reads what it needs from the remote itself: the host from the address, and the default branch and the branches from git.

The repository is cloned the first time the run needs it, with its last 50 commits.

The credential comes from the code host tool you bind

The clone and every push use the code host tool your enterprise or team binds on Tools. A workspace reads these custom props off the binding: key → value settings added behind Custom props on its form.

Key What it is GitHub's
git.host The host this binding's token is valid for github.com
git.username The username git sends alongside the token x-access-token
git.repoTemplate A clone address with {repo} in it, applied to a short owner/name input https://github.com/{repo}.git
git.repo The repository the binding's layer works in, for a run whose input names none Usually left out

The workspace takes the binding, among those the run reaches (task, role, member, team, enterprise, in that order), whose git.host matches the remote's. Each tool counts once: the nearest binding of it the run reaches, so a member's own binding without the props hides the enterprise's. Its token is given to git scoped to that host, and only over https://, so a prompt from any other host is answered with nothing. A remote on a host no binding names is cloned with no credential: a public repository still clones, and nothing pushes. The GitHub guide says what to set for GitHub.

A binding may also name the repository its layer works in (git.repo), and then a run needs no repo input at all; a run that gives one overrides it. Because a binding can carry git.repoTemplate, a short input keeps working:

{ "repo": "acme/website" }

A run with no repository — neither in its input nor on a binding it reaches — fails workspace_repo_missing before it calls anything. A run that names a short owner/name no binding's git.repoTemplate expands fails workspace_repo_no_clone_address when it opens its workspace: the OS never guesses a host for it. Tools says when tasks work in a workspace and no binding has git.* props, and flags a binding without them when another binding of the same tool has them.

The branch

A run works on one branch, chosen in this order:

The run works on When
Its branch input The run was started with one, or the task before it handed one on.
The task's branch template The task's workspace sets branch. Each {key} in it is filled from the run's input: fix/{issue} with { "issue": 42 } is fix/42.
os/<task slug>-<first 8 characters of the run id> Neither. A new branch.

A template with {issue_slug} in it is worked out once, from GitHub. When the run's input names a pull request (number) and the task can read pull requests, the run takes that pull request's own branch. Otherwise it names the branch after the issue in the input (issue): os/<issue number>-<the issue's title as a slug>. The name is stored as the run's branch input and every task after it inherits it, so renaming the issue later never moves the branch. The task has to be able to read the issue or pull request (github.issue_read, github.pull_request_read, or all of GitHub); if the branch cannot be worked out, the run fails workspace_branch_unresolved before it starts work.

A branch that exists on the remote is checked out as it is there. One that does not is created from the default branch: the branch the clone checks out, which is the repository's own default.

Kept up with the default branch

When the run's branch already exists and is behind the default branch, the default branch is merged into it and pushed before setup runs and before the member does anything, so an open pull request never falls more than one run behind. It is an ordinary merge and an ordinary push. If the merge conflicts, or the remote refuses the push, it is undone and the branch is left exactly as the remote has it, for the run to deal with.

What the repository provides

How the code is set up and checked belongs to the repository, not to the task. Tasks are shared between enterprises through the catalog and pointed at other repositories, so a task's skill never names one repository's scripts.

Setup

After cloning, the workspace runs the repository's setup. With no setup file, it works out what to run, and runs both lines when both apply:

The repository has Setup runs
mise.toml or .tool-versions mise install
package.json with a packageManager of npm, and no pnpm-lock.yaml npm ci, or npm install without package-lock.json
Any other package.json pnpm install --frozen-lockfile, or pnpm install without pnpm-lock.yaml

To choose for yourself, add .os/workspace.yml (or .os/workspace.yaml, or .os/workspace.json) at the root of the repository:

# Leave setup out to use the list above. `setup: []` skips setup altogether.
setup:
  - [pnpm, install, --frozen-lockfile]
  - [pnpm, run, generate]
teardown:
  - [node, scripts/clean-up.mjs]
  • Each step is one line, - [binary, argument, …]: no shell, and no other YAML form. In this form an argument cannot contain a comma.
  • The binary is one of git, node, npm, pnpm and mise. Anything else a repository needs (another language, a particular version of Node) comes through mise: declare it in mise.toml, and mise install installs it.
  • setup runs in order after every clone. If a step exits non-zero, the clone is thrown away and the workspace call fails with workspace_setup_failed, naming the binary and the start of its output. The member is handed the error, and its next workspace call tries again.
  • teardown runs when the workspace is deleted. A step that fails there is ignored.
  • The JSON form is the same two lists: { "setup": [["pnpm", "install"]], "teardown": [] }.

Git hooks

The repository's own git hooks run on the commits and pushes a member makes, once setup has installed them (a prepare script that installs hooks runs as part of pnpm install or npm ci). The workspace tools never skip them, and refuse the usual ways around them. A hook that fails refuses the commit or the push, and its output goes back to the member to fix.

Commands for the engineering tasks

The engineering tasks in the catalog (writing an issue's tests, implementing it, validating it) never run a repository's scripts by name. They call three mise tasks, and the repository's mise.toml says what each one does:

Command Called as What it has to do
os:red mise run os:red <issue> Run the issue's tests: the test files the branch adds or changes. Print each file's path. Exit 0 when every test passes, 3 when the branch has no tests to run, anything else when a test fails. Its mise description says where this repository keeps tests and which framework runs them: the task that writes tests reads it.
os:check mise run os:check [issue] Everything to run once before committing: lint, typecheck, tests, and whatever build they need first. Exit 0 when it all passes.
os:coverage mise run os:coverage [issue] Measure test coverage of the lines the branch changed, against the repository's own thresholds. Exit 0 when they are met; otherwise list the uncovered lines.

os:check and os:coverage may also exit 6 when every failure is a test that fails on the default branch too, printing each as pre_existing_failure: <file> › <test>. The tasks then treat the default branch as broken rather than the pull request, and look again later.

A repository with no os:red stops those tasks with a message saying it needs one. Any task can run whatever else the repository's mise.toml defines, through workspace.run_command.

A check has to fit one command

Every command a member runs has limits: ten minutes, and 32,000 characters of output. A check that runs past the first is stopped before it can say what failed, and one that prints past the second is cut short. So:

  • Print what failed, not everything. Keep the full output in a file in the clone and print where it is. A member can read the file when it needs to.
  • Split a check that cannot finish in ten minutes. mise run os:check --parts prints the name of each part, one per line, and mise run os:check --part <name> runs that part over the whole repository, with the same exit codes. A task that checks a whole branch asks for the parts and runs each in turn. A repository that does not answer --parts is checked with one mise run os:check.

Commands for a dependency refresh

Tasks that keep a repository's dependencies current call three more mise tasks. They are optional: a repository without them is not offered a refresh. Which package manager it uses, what counts as out of date and how a version is changed are the repository's own. The tasks name no package manager, so the same tasks work on any repository that answers these commands, and a repository can cover container images or CI actions by changing its own script.

Command Called as What it has to do
os:deps mise run os:deps Print what is out of date, in the form below. Exit 0 when the report is complete, with or without updates. Exit non-zero when the scan could not be completed: never print an empty report for a scan that failed.
os:deps:apply the lines os:deps printed Move each named dependency to exactly the version named, and update the lockfile. Skip one that is already there, so a line can be run again. Exit non-zero, changing nothing, when an argument matches nothing the repository declares.
os:deps:verify mise run os:deps:verify Exit non-zero, naming each one, when a dependency now resolves lower than on the commit the branch started from.

The report is markdown, because a task copies parts of it into issues as they are:

updates: 2 security: 1 majors: 1 held: 1

## Updates to apply

| Package | Current | Target | Level | Security | Note |
| --- | --- | --- | --- | --- | --- |
| `left` | 1.0.0 | 1.0.1 | patch | high | |
| `right` | 2.1.0 | 2.2.0 | minor | | |

Apply in this order, running the checks after each group:

1. Security: `mise run os:deps:apply [email protected]`
2. Minor: `mise run os:deps:apply [email protected]`

## Majors

### Major upgrade: framework 3 to 4

- `framework` 3.2.0 to 4.1.0 (released 2026-08-25)
- Release notes: https://example.com/framework/releases
- Apply: `mise run os:deps:apply --major [email protected]`

## Held back

- `odd` 1.0.0: the registry names no version to compare

What the tasks rely on:

  • The first line is the counts. updates and majors both 0 means there is nothing to do.
  • ## Updates to apply holds the changes that are safe to make together: a table, then one runnable mise run os:deps:apply ... line for each group, numbered in the order to apply them. These become one issue, and one task runs the lines exactly as printed. What an argument looks like is the repository's choice.
  • ## Majors has one ### heading for each upgrade that may break the code, in the order to take them. The heading is used as the issue's title, so it must read the same on every scan for as long as that upgrade is on offer. A person rejects an upgrade by closing its issue as not planned, and a title that is already on an issue, open or closed, is never filed again. Under the heading: what moves, where to read what changed, and its own apply line.
  • ## Held back names anything the scan could not decide, with the reason. Nothing is done with these.
  • The whole report fits in 32,000 characters. When it would not, leave out the least urgent updates and say how many were left out.

Deciding what is safe is the repository's job too. Things worth deciding there: how old a release must be before it is offered, whether a fix for a known vulnerability waits that long, and never offering a prerelease or a withdrawn release.

What a task decides

A task version's Workspace decides which branch its runs work on and which files they may change. On a task in the portal it is Workspace JSON:

{
  "branch": "fix/{issue}",
  "writeAllow": ["src/**", "tests/**"],
  "writeDeny": ["src/generated/**"]
}
Field What it does
branch The branch template (see The branch).
writeAllow Paths the run may change freely. Once it is set, every path it does not match is refused, unless appendOnly matches it.
rewriteAllow Paths the run may change freely, without refusing everything else.
writeDeny Paths the run may never change. It wins over every other field.
appendOnly Paths the run may add files and lines to, but where no line that was there when the run started may change or go.
requireFiles Paths that must exist in the clone before the run starts work; each {key} is filled from the run's input. If one is missing, the run fails workspace_file_missing.

Paths are globs: * stays inside one folder, ** crosses folders. A task with none of the path fields set may change anything in the clone.

The rules are checked when a file is written, edited or deleted, again on what a commit would include (a command can change files too), and, for appendOnly, once more on the run's own commits when it pushes. A refused write changes nothing; a refused commit commits nothing. During a merge, a file the merge brings in may be written and committed as the merge has it, whatever the rules say, so a run can finish a merge; a line of the run's own in such a file is still refused.

Commits and pushes

Commits are authored and committed as the member the run belongs to: its name and the email address on its member page. A run with no member commits as the enterprise itself, under your enterprise's name, from an os@ address on your enterprise's Zero Human mail domain.

Code hosts link a commit to an account by its email address. For commits to show under the member's own account on your host, add the member's address to that account and verify it (for GitHub, see GitHub). The push itself is made by whichever account the credential belongs to.

workspace.git_commit stages what the run changed and commits it; a new file that only a command created may need a git add first. workspace.git_push pushes the branch to the repository's remote under the same name, as an ordinary push: it never overwrites work someone else pushed.

If a run of a task that lists workspace.git_push finishes without a successful push while its workspace holds changes or commits the remote does not have, it fails: git_push_missing, or the reason the commit or push was refused. Work is not lost behind a success. If a run runs out of steps, or its model call fails, the OS commits and pushes what it had before the run fails, where the task's rules allow, so a retry picks up from there.

What is never allowed

  • Pushing the default branch. workspace.git_push refuses the repository's default branch, and any branch named main or master (default_branch_refused). A member has no other way to push: workspace.run_command refuses a command with git push anywhere in it, including through mise, pnpm or node (git_push_via_run_command).
  • Skipping the repository's hooks. A git command with --no-verify, git commit -n, or a core.hooksPath override is refused (git_hooks_skip_refused), and the file tools write nothing under .git/hooks.
  • A shell. workspace.run_command runs one binary with its arguments: no pipes, no redirects, no cd, no &&. The binary is one of git, node, npm, pnpm and mise, named, not given as a path (bin_not_allowlisted).
  • Files outside the clone. Every path the file tools read or write is inside the clone. A path that resolves outside it is refused.

What waits for you

Nothing a run does in its workspace waits for you. The OS answers every workspace.* and memory.* call before it reads the task's gates, so a gate declared on one of those tools is saved but never waits: every write, commit and push goes ahead as the task's rules allow, and only ever to a branch that is not the default one. See Gates and grants in the built-in tools reference.

The decision that waits is the merge. Getting a branch into the default branch goes through your code host: a pull request, or merge request, and the host's merge tool, which a task that merges should gate. On GitHub it has to: a task that names github.merge_pull_request cannot be saved without a gate on it. See Gates and tool scopes.

Planned

What a merge needs will be set on the task's own gate, for any host's merge tool: that the gate is required, that the run writes a summary before it waits, and that approving it ends the run. The OS will not know GitHub's merge tool by name.

Limits

Limit Value
One command 10 minutes, then it is stopped with everything it started.
A command's output At most 32,000 characters go back to the member. The exit code always does.
Setup and teardown 8 steps each, each at most 16 words.
Clone depth The last 50 commits.
Reading without changing 10 repeated reads (the same file, folder or search again, or an ad hoc command) with no write, edit or commit between them, and a per-run budget of 150 distinct files, folders and searches. The next one past either is refused (exploration_budget_exceeded), and the member is told to act on what it has read.
Search 100 matches by default, 500 at most.

Running mise run …, git status, git diff, git log or git commit is progress, not reading: it starts the count of repeated reads again. Nothing resets the per-run budget, but a file, folder or search the run has already read never counts against it twice.

Troubleshooting

What you see Why What to do
workspace_setup_failed A setup step exited non-zero Run the same step in a fresh clone of the repository and fix it there, or change .os/workspace.yml
workspace_manifest_invalid .os/workspace.yml is not in the form above One step per line, - [binary, argument], under setup: or teardown:
workspace_setup_bin:<name> A setup step uses a binary that is not allowed Run it through mise, or a package.json script
tool_auth_denied:workspace.… The host refused the clone or the push Check that the code host binding's account can write to the repository, and that its token has not expired
workspace_repo_missing Neither the run's input nor a binding it reaches names a repository Give the run a repo, or set the repository on the code host binding
workspace_repo_no_clone_address The run names a short owner/name, and the nearest code host binding it reaches has no git.repoTemplate to expand it Add git.repoTemplate under Custom props on that binding on Tools, or give the run a full repo address
The clone asks for a password, or the push is refused No binding the run reaches names the remote's host Add git.host and git.username under Custom props on that code host's binding, which holds the token — see Where the repository lives
default_branch_refused The run was working on the default branch Give the task a branch template, or start the run with a branch input
git_push_missing The run finished with work it never pushed Read the run's last commit and push calls: a refused commit or push says why
workspace_write_not_allowed or workspace_commit_not_allowed A path outside the task's rules Change the task's Workspace, or the work
workspace_append_only The run changed or removed a line in an add-only path The run has to add lines, not change them, or the path needs writeAllow