Context resolution
This page documents how Untether resolves run context (project, worktree/branch, engine) from messages. For step-by-step usage, see Projects and Worktrees.
This page documents how Untether resolves run context (project, worktree/branch, engine) from messages. For step-by-step usage, see Projects and Worktrees.
Overview
Projects let you give a repo an alias (used as /alias in messages) and opt into
worktree-based runs via @branch.
- If no projects are configured, Untether runs in the startup working directory.
- If a project is configured,
@branchresolves/creates a git worktree and runs the task in that worktree. - Progress/final messages include a
dir:footer when project context is active.
Config schema (relevant subset)
All config lives in ~/.untether/untether.toml.
See Config for the full reference.
untether config
untether config set default_engine "codex"
untether config set default_project "z80"
untether config set transport "telegram"
untether config set transports.telegram.bot_token "..."
untether config set transports.telegram.chat_id 123
untether config set projects.z80.path "~/dev/z80"
untether config set projects.z80.worktrees_dir ".worktrees"
untether config set projects.z80.default_engine "codex"
untether config set projects.z80.worktree_base "master"
untether config set projects.z80.chat_id -123
toml
default_engine = "codex" # optional
default_project = "z80" # optional
transport = "telegram" # optional, defaults to "telegram"
[transports.telegram]
bot_token = "..." # required
chat_id = 123 # required
[projects.z80]
path = "~/dev/z80" # required (repo root)
worktrees_dir = ".worktrees" # optional, default ".worktrees"
default_engine = "codex" # optional, per-project override
worktree_base = "master" # optional, base for new branches
chat_id = -123 # optional, project chat id
Legacy config note: top-level bot_token / chat_id are auto-migrated into
[transports.telegram] on startup.
Note on worktrees_dir:
- The default
.worktreeslives inside the repo root. You’ll see it as an untracked directory (with nested git worktrees) unless you ignore it. - Options:
- add
.worktrees/to your repo.gitignore, or - set
worktrees_dirto a path outside the repo (e.g.~/.untether/worktrees/<alias>). - add it to
.git/info/excludeif you prefer a local-only ignore.
- add
Validation rules:
projectsis optional.- Each project entry must include
path(string, non-empty). default_enginemust be an available engine id (it defaults to"codex"); an unknown value is a startup error.- The project rules below don’t stop startup: a project that breaks one is skipped and logged
(
project.skipped.*), and the rest still load.- Project aliases can’t collide (case-insensitively) with engine ids or reserved chat commands
(
cancel,continue,file,new,agent,model,reasoning,trigger,topic,ctx), or with each other. - A per-project
default_enginemust be an available engine id. projects.<alias>.chat_idmust be unique and must not matchtransports.telegram.chat_id.
- Project aliases can’t collide (case-insensitively) with engine ids or reserved chat commands
(
default_projectmust match a loaded project alias; otherwise it’s ignored and logged (projects.config.invalid_default_project).transportdefaults to"telegram"when omitted; override per-run with--transport.
untether init
untether init <alias> registers the current repo as a project alias.
Important behaviour:
- The stored
pathis the main checkout of the repo, even if you rununtether initinside a worktree. Untether resolves the repo root via the git common dir and writes that path to[projects.<alias>].path. worktree_baseis set from the current repo using this resolution order:origin/HEAD→ current branch →master→main.
Directives and context resolution
Untether parses the first non-empty line of a message for a directive prefix.
Supported directives:
/<engine-id>or/<engine-id>@bot: chooses the engine/<project-alias>: chooses a project alias@branch: chooses a git branch/worktree
Rules:
- Directives must be a contiguous prefix of the line; parsing stops at the first non-directive token.
- At most one engine directive, one project directive, and one
@branchare allowed (duplicates are errors). - If a reply contains a
dir:line, Untether ignores new directives and uses the reply context.
Context footer (dir:)
When a run has project context, Untether appends a footer as part of the 🏷 info line:
- With branch:
dir: <project> @<branch> - Without branch:
dir: <project>
The dir: line is parsed from replies and takes precedence over new directives.
For backwards compatibility, Untether also accepts the older ctx: format when parsing replies.
When a message arrives in a chat whose chat_id matches projects.<alias>.chat_id,
Untether defaults the project context to that alias unless a reply dir: or explicit
/<project-alias> directive is present.
In non-topic chats, /ctx can bind a chat context. That bound context is treated as
ambient and takes precedence over the default project mapping until cleared.
In a forum topic, the context bound with /topic (merged with the chat’s project) is the
ambient context instead.
Precedence, highest first:
- a
dir:(orctx:) line in the replied-to message /<project-alias>and@branchdirectives- the ambient context (topic binding, else
/ctxbinding) - the chat’s project (
projects.<alias>.chat_id), elsedefault_project
An @branch on its own keeps the project from the ambient context or default; an ambient
branch only applies when the resolved project matches the ambient project.
Worktree resolution
When @branch is present and names the branch already checked out in <project.path>,
Untether runs in the main checkout. Otherwise:
worktrees_root = <project.path> / <worktrees_dir>
worktree_path = worktrees_root / <branch>
Branch validation:
- Must be non-empty
- Must not start with
/ - Must not contain
..path segments - May include
/(nested directories) - The resolved worktree path must stay within
worktrees_root
Worktree creation rules:
- If
worktree_pathexists:- It must be a git worktree or Untether errors.
- If it does not exist:
- If local branch exists:
git worktree add <path> <branch> - Else if remote
origin/<branch>exists:git worktree add -b <branch> <path> origin/<branch> - Else:
git worktree add -b <branch> <path> <base>
- If local branch exists:
Base branch selection:
projects.<alias>.worktree_base(if set)origin/HEAD(if present)- current checked out branch
masterif it existsmainif it exists- otherwise error
When @branch is omitted:
- Untether runs in
<project.path>(the main checkout).
Sessions and follow-ups
Context decides where a run happens; the conversation scope decides which session a message continues when it carries no resume line.
- A reply to a message with a resume line continues that session (the resume line wins over any stored session). A reply to a progress message whose run is still going queues behind that run.
- Forum topics (
[transports.telegram.topics] enabled = true): each topic stores its own session per engine. session_mode = "chat": sessions are stored per scope and engine — per chat in a private chat’s main thread, per topic in a private chat’s topics (#734), and per sender in a group without topics. The topic store wins when both apply.session_mode = "stateless"(default): without topics, each message starts a new session unless it carries or replies to a resume line.
Follow-up mode (#775, Claude Code only)
decides what a message sent while a run is active does: queue waits for the running turn to end,
steer writes it into the running session. It resolves in this order:
- the topic’s setting (
/steer//queuewith no text, or/config) - the chat’s setting
[transports.telegram] followup_modequeue
Examples
Start a new thread in a worktree:
/z80 @feat/streaming fix flaky test
Reply to a progress message to continue in the same context:
dir: z80 @feat/streaming