Skip to content

Operate

Source: harness/docs/client/operate.md Status: current

Day-to-day use of the harness TUI. Named actions, need-human keys, permission mode, /connect / /mcp / /model.

Not here: pack authoring (author/), PEW/Proposal law (../canon/proposals.md), approval mechanism (../runtime/approval.md), shell duties and empty-draft law (shell.md), tape grammar (transcript.md).


You evaluate named actions on the session socket (turn, kv/set, connect/adopt, …), write PEW/Proposals (cancel / Question), and write approved on a pending Approvable leaf. The daemon writes Job and PEW bodies.

Permission mode is manual / auto / yolo. Session mode (act / plan) is not that join. Slash commands are app tools; they do not replace the agent loop. Law: ../runtime/approval.md.


Axis Values When
Permission mode manual, auto, yolo All three run policy.yaml. Remainder ask: cover / classifier / allow. Floor block holds in all three.

Shift-Tab cycles manual → auto → yolo → manual. It does not cycle session mode. Active permission mode is SessionRoot.currentPolicy. The same write is last-choice backing: session policy/set also stores ProjectRoot.defaultPolicy and kernel config.json defaultPolicy (not a kv key). A new session mints project.defaultPolicy if set, else kernel, else manual. Attach does not re-seed. evalSpec overlays the turn and does not write last-choice.

A currentPolicy change re-runs still-null ApprovableJobs (not mid-evaluate), including rows already asked. In-flight evaluation does not rebind.


When policy asks, you write approved on that leaf (approvalQueueHead). You do not mint a second “approval” unit. Ask chrome shadows the composer — the prompt you were typing stays. Confirm chords write only while the cover is armed; a mount over a nonempty draft waits KernelRoot.approvalAskThrottleMs (default 2s, 0 = off). No y / n / a / Space grants. Implementer face: shell.md. Extra-root folder offer: ../runtime/approval.md.

Persist modifier is ⌃ (config.json superKey, default ctrl; cmd is ⌘). Super is not inferred from the OS — terminals often do not forward it.

You press / mean What happens
Allow (empty cover, ⏎) approved = true on the leaf. A file read also stores that path on SessionRoot.readAllowlist — later reads of it (and descendants) this session do not ask. Writes still ask.
Allow this folder (empty cover, extra-root read, suggested parent, persist-mod ⏎) same, plus readAllowlist for that parent. Offered when a same-parent sibling was allowed or is in-flight. Not Always. Not auto.
This session in auto (empty cover, auto remainder ask of a grantable cluster, persist-mod ⏎) same, plus SessionRoot.autoAllow[cluster] = true. Hatch / extra-root deny / MCP cousin: unpainted.
Always in auto (empty cover, same leaf, persist-mod ⇧ ⏎) same, plus kernel config/auto-allow (config.json autoAllow)
Allow this session (empty cover, MCP look-up in manual, persist-mod ⏎) same, plus SessionRoot.toolAllowlist for that connection+tool
Always allow (empty cover, MCP look-up in manual, persist-mod ⇧ ⏎) same, plus kernel connect/always-allow (connection alwaysAllow)
Deny (empty cover, Esc) approved = false on the leaf. The Job seals with no BODY — it still happened on the tree
Deny this session (empty cover, MCP cousin, persist-mod Esc) same, plus SessionRoot.toolBlocklist
Deny forever (empty cover, MCP cousin, persist-mod ⇧ Esc) same, plus kernel connect/exclude-tool (!name on connection tools). There is no alwaysDeny bag.
Allow / deny with comment (cover has text, persist-mod ⏎ / persist-mod Esc) writes approvalComment too. Persist and folder-widen do not fire from a comment. Shift+Enter is a newline.
Esc (open head, nothing need-human) empty Esc arms, then turn/cancel. Nonempty Esc is draft-clear (same arm).
Choice row (Question) propose a choice. Actor considers. Not UAC.

Policy-written allow/deny does not raise the cover; it is a chip on the tool row (policyResult / judgeResult). Persist does not cross the floor — a look-up cannot be banned from the card, a cousin cannot be remembered as a read. Folder-widen is not Always and does not fire on a lone file.


Connection inventory only. Source of truth is ~/.harness/config.json; KernelReflector projects it onto the kernel CRDT. Picker: rename / remove / adopt. Class/instance = driver/connection. Esc and back are host navigation, not menu rows. Secrets stay local to the daemon.

Selecting a wire also sets that session’s model (last-used, else the highest-version live id) and remembers both as the default. MCP catalog cut is tools on that row — /mcp.


MCP connections in the same ~/.harness/config.json. /mcp is health, authenticate, add/drop, and session disable (kv/set mcp-disabled:{id} — not a file write). The durable catalog cut is tools on the connection; toolSeverity overlays auto-class on leftovers. Always-as-read names land on alwaysAllow via kernel connect/always-allow. Deny forever appends !name to tools via kernel connect/exclude-tool. Session remember / hide live on SessionRoot.toolAllowlist / toolBlocklist. The kernel rewrites config.json the same way connect/adopt does.

Hide is not a license. Remaining tools still run policy.yaml. Connecting is not a grant. A hidden name is absent from the next turn’s catalog; calling it by wire name still fails.

Ordered globs on the server tool name (get_issue, merge_pull_request) — not the wire mcp__github__…. * any run of characters; ? one character. Match is case-sensitive. ! in front removes matches. Rules apply in listed order, so a later include can put a name back. A list of only excludes behaves as if * were written first. Omit the field (or a non-array) to keep every tool the server lists. [] keeps none.

Write it on settings.tools, or as a sibling tools on the connection object (lifted into that bag):

{
"connections": {
"github": {
"driver": "mcp-stdio",
"name": "GitHub",
"settings": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"tools": ["!merge_*", "!delete_*"]
}
},
"mail": {
"driver": "mcp-http",
"settings": {
"url": "https://mcp.example/mcp",
"tools": ["get_*", "list_*", "!get_secret"]
}
}
}
}

github keeps everything except merge_* / delete_*. mail starts empty, adds get_* and list_*, then drops get_secret. /mcp row counts are the server’s listed tools.

Optional overlay when auto-class is the wrong pole. Keys are serverToolName globs; last match wins. Same placement as tools. Values: view → catalog read; effect → effect (auto cannot say this); irreversible-effect → write. Omit to keep auto-class. A bad value is skipped. Not a license and not readOnlyHint.

"toolSeverity": {
"mark_all_notifications_read": "effect",
"merge_*": "irreversible-effect",
"get_issue": "view"
}

Exact serverToolName strings remembered by Always allow. Kernel connect/always-allow writes the same bag as tools. The read floor still applies — a veto or one-word name on this list does not skip ask. A new name still knocks.

"alwaysAllow": ["get_issue", "list_issues"]

Session model, and the default. Models are invocation ids on a live connection ((:models (:live c))), plus free-type id. Sets the session KV key model (and the connection when you pick a model on a wire) with remember: "1", so the next session mints the same pair. A bare kv/set — a subagent, or --model / --connection — changes only this session.

Does not adopt or drop connections — use /connect.


Daemon and TUI are one process; it appends ~/.harness/harness.log (or <kernel>/harness.log if you passed --kernel). Override with HARNESS_LOG=/path/to/file. Nonempty DEBUG also mirrors chatter to stderr before the alt-screen.


Doc Role
shell.md Client shell duties, empty-draft law, unattended eval
transcript.md Live tape: identity, liveness, ask pin, follow-tail
../canon/proposals.md PEW/Proposal shapes
../canon/standard-toolkit.md Default FS tools
author/ Pack / command authoring