Programs in Jun's terminals, agents above all, can ask the Jun that runs them about its terminals, and change what it shows, with jun tool. An agent you work with, in a project of its own, can then see what the other agents are doing, read their screens, bring you a terminal or a branch's changes to look at, pass a message on to another agent, and run commands in terminals of their own, once or again and again. Jun gives the tools only; how an agent uses them, as a coordinator or otherwise, is up to the instructions or skill you give it.
"$JUN_BIN" tool help prints what this page says, for an agent to read.
Two skills build on these tools, in the Agent Skills format, which Claude Code, Codex and Pi read:
jun-toolsteaches an agent the tools: when to use them, the common work, and the pitfalls.jun-deputymakes a session your deputy: it hears of your agents as they change, looks them over on a schedule, and reports what needs you.
Install them for your user with the skills CLI:
npx skills add bencode/jun -gOr copy the folders in skills/ to ~/.claude/skills/ for Claude Code ($CLAUDE_CONFIG_DIR/skills/ when that is set), or to ~/.agents/skills/ for Codex and Pi.
| Command | Output |
|---|---|
jun tool help |
The commands, fields, environment and exit status |
jun tool terminals |
Jun's running terminals, as a JSON array, by id |
jun tool watch |
Agents' status as it changes, as JSON, a line each, until it ends |
jun tool screen <id> [--lines N] |
The last N lines of the terminal's text, as plain text |
jun tool open <target> [--in <perspective>] [--peek] |
Shows the target. Prints nothing. |
jun tool close <target> |
Closes the target. Prints nothing, or what it leaves pending. |
jun tool back / jun tool forward |
One step back or forward through where you have been, as ⌘[ and ⌘] go. Prints nothing. |
jun tool send <id> <text> / jun tool send <id> - |
Submits the text to the agent in the terminal. Prints nothing, or sent, not started yet. |
jun tool message [--title <title>] [--kind <kind>] [--link <resource>] <text>, or - for the text |
Shows the text to the user, in Messages. Prints the message's id. |
jun tool message update <id> [--kind <kind>] [<text>], or - for the text |
Changes the message's kind, adds a reply, or both. Prints nothing. |
jun tool message |
The messages, as a JSON array, the latest first |
jun tool run [--dir <path>] [--open] <command…> |
Starts a terminal running the command. Prints its id. |
jun tool loop <every> [--dir <path>] <command…> |
Runs the command every interval in a terminal of its own. Prints the loop's id. |
jun tool loop |
The loops, as a JSON array |
jun tool loop cancel <id> |
Ends the loop and its terminal. Prints nothing. |
Run them as "$JUN_BIN" tool …, which works whatever PATH and aliases say.
[
{
"id": 3,
"project": "web",
"dir": "/path/to/web",
"title": "Fix the login form",
"agent": "claude",
"status": "waiting",
"self": false
}
]| Field | Meaning |
|---|---|
id |
The terminal's id, for screen. Ids hold while Jun runs; a new run numbers its terminals anew. |
project |
The project the terminal belongs to |
dir |
Its shell's directory |
title |
Its title, as the Agents view or its tab shows it |
agent |
claude, codex or pi, when one runs in it; absent otherwise |
status |
waiting, working or idle, as the Agents view shows it; with agent only |
self |
true for the terminal the caller runs in |
Terminals with closed tabs are listed too: their shells keep running. Terminals whose shell has ended are not.
Runs until Jun quits, printing a line as soon as there is news of an agent:
{"id":3,"project":"web","agent":"claude","title":"Fix the login form","from":"working","to":"waiting"}| Field | Meaning |
|---|---|
id, project, agent, title |
As terminals has them |
from |
The status before; empty for an agent that is new |
to |
The status now; empty for an agent that is gone, or whose terminal closed |
- It starts with a line for each agent,
fromempty, so that a change just before it began is not lost. - When Jun tells: Jun reads the agents' status every second.
waiting,working, a new agent and one gone are told at once.idleis told once it has held for 3 seconds: agents go idle for a moment between steps, and that is no news. - It ends with status 1 when Jun quits, or when the reader falls too far behind, for Jun never waits for a reader. Run it again to catch up, from a loop for instance.
- Filter with
jq, which needs--unbufferedin a pipeline to pass each line on as it comes:
"$JUN_BIN" tool watch | jq --unbuffered -c 'select(.to == "waiting")'- The terminal's text with its scrollback, after the blank lines below the cursor: 40 lines by default, 1 to 500 with
--lines. - A line the terminal wraps on screen counts as one.
- An agent's text includes what it draws around its messages: frames, status bars and its input box.
A resource names something in Jun. open and message --link take these forms, and close all but project:. open and close also take the perspective: and peek of the table below, which are no resources: they depend on where the user is.
| Resource | What it is |
|---|---|
terminal:<id> |
A terminal, with the id that terminals and watch give |
diff:<id> |
The changes of the branch that terminal is on: the same id |
file:<path> |
A file. A relative path starts from the caller's directory |
project:<name> |
A project, whatever the case of its name |
| Target | open |
close |
|---|---|---|
terminal:<id> |
Shows the terminal's tab, switching project and perspective as needed, and reopens its tab when it was closed. With --peek, an agent's terminal shows in the peek drawer instead, or its tab when that is on screen. |
Closes its tab in the current perspective. The shell keeps running. |
diff:<id> |
Opens the changes of the branch the terminal is on, as its agent card's Review Changes does, in the current perspective. With --in <perspective>, first switches to that perspective of the terminal's project, made when there is none. |
Closes the Diff of that branch in the current perspective. |
file:<path> |
Opens the file in a tab of its own, read-only, wherever it is: see Files. Opened again, the file is read again. | Closes its tab in the current perspective |
project:<name> |
Switches to the project | — |
perspective:<name> |
Switches to the perspective of the current project, made when there is none | Deletes it, as Delete Perspective… does: with terminals in it, Jun asks you to confirm, and prints waiting for the user to confirm. A project keeps at least one perspective. |
peek |
— | Closes the peek drawer |
- Names match whatever their case.
- The first time a project's repository is asked for, Jun reads its branches and worktrees;
diff:waits a few seconds for them. When the terminal is in none of the worktrees read, as when its agent made one since, Jun reads them again once.
A review, for instance, takes two calls: open diff:7 --in Review, and once you are done, back.
open, close, back and forward change what you see, so Jun runs them only:
- For the terminal you are with: the one shown in the peek drawer, or else the terminal you last focused. Otherwise:
the user is not with you. - While no dialog waits for you: the command palette, a form, a confirmation, an error or the shortcuts panel. Otherwise:
Jun is waiting for the user. - On another terminal: a terminal cannot open or close itself (
that is your own terminal).
Opening another terminal gives it the focus, so you are then with that terminal: the caller's next change is refused until you come back to it.
- The text goes in as a paste, then Enter, as when you paste a message and submit it: its line breaks stay in one message. Checked with Claude Code 2.1.318, Codex 0.166.0 and Pi 1.5.0.
- With
-, the text is standard input, for long or multi-line messages. Line breaks at its end go; at most 1 MiB. - Any agent takes it, whatever it is doing, the caller's own terminal too. A terminal running no agent does not: text there would run as a command.
- Once sent, Jun waits up to 5 seconds for the agent to work. If it does not by then,
sendprintssent, not started yet, still with status 0.
message tells the user something without disturbing them: it appears in Messages, in Global.
"$JUN_BIN" tool message --title 'Asks to open a PR' --kind ask --link terminal:7 'I suggest yes.'- Who may send: any terminal of Jun, at any time, whatever the user is looking at. Outside Jun's terminals it is refused.
--title: one line, at most 200 bytes. With a title, the text may go.--kind:askwhen the user has something to do,warn,done, orinfo, the default. It gives the message its icon and color.--link: the resource it is about, where a click takes the user. Jun tells the project from it, so a loop reporting on every agent gives only this. Without it, the message is plain news: no tag, and a click does nothing.- A resource that does not exist is status 1, and shows nothing: the terminal, file or project, or a terminal in no worktree for
diff:.peekandperspective:are not for messages. - The text: with
-, standard input. Line breaks at its end go; at most 4 KiB. - How long it stays: Jun keeps the last 500, until it quits. A terminal's id lasts as long, so a message's link never points at another terminal.
- Its id:
messageprints it. Jun shows it asM12; the commands take12.
message update changes what the user sees of a message: its kind, a reply below it, or both, at least one of them.
"$JUN_BIN" tool message update 12 --kind done 'You agreed; it goes on.'
"$JUN_BIN" tool message update 12 'PR #53 is open, the tests pass.'
"$JUN_BIN" tool message update 12 --kind doneaskmeans the user has something to do, soaskbecomingdonetells them it is handled. Jun does not do this itself: whoever watches the agent decides.- A reply has its own time, and moves the message to the top. A new kind alone leaves the message where it is.
- The text: as for
message. A message keeps its last 50 replies. - Who may update: any terminal of Jun, whoever sent the message. Outside Jun's terminals it is refused.
- A message that does not exist is status 1, as is one Jun no longer keeps.
message with nothing after it lists them, for instance to find what still waits for the user:
"$JUN_BIN" tool message | jq -c '.[] | select(.kind == "ask")'[
{
"id": 12,
"time": "2026-11-05T19:30:00+08:00",
"kind": "done",
"title": "Asks to open a PR",
"text": "I suggest yes.",
"project": "web",
"link": "terminal:7",
"replies": [
{ "time": "2026-11-05T19:32:00+08:00", "text": "You agreed; it goes on." }
]
}
]link is as --link takes it; title, text, project, link and replies are absent when there are none.
Both run a command in a terminal of the caller's project, in --dir, by default the caller's current directory. The terminal has no tab: open terminal:<id> shows it, and terminals lists it.
- The command: the words after the options, joined by spaces into one command line, as
sshjoins them. Quote a command whose own quotes must stay:run 'git commit -m "a b"'. - run types the command into a new shell, as if you had, so your shell's configuration applies; the shell stays once the command ends.
run codexstarts Codex there. With--open, the terminal shows in a tab, focused, under the same rules asopen. - loop runs the command again and again, each run the interval after the one before ends, so that runs never overlap. The interval is written as
90s,5mor1h, at least1m.- It runs with
$SHELL -l -c, as a script runs: your login files apply, not.zshrc. Write absolute paths, or"$JUN_BIN". - Each run shows in the loop's terminal with its time and exit status.
- A loop's id is its terminal's, for
screenandopentoo.
- It runs with
- Ending a loop:
loop cancel <id>, Control-C in its terminal, or quitting Jun. Loops are not saved.
Jun runs what it is asked: a loop may run run, or start another loop. A loop that sends to an agent wakes it as send does:
"$JUN_BIN" tool loop 10m "$JUN_BIN" tool send 7 "'How is it going? Report anything blocking you.'"send presses Enter whatever the agent shows, so a loop that may meet a question should check the agent's status first.
[
{
"id": 12,
"every": "10m",
"command": "/path/to/jun tool send 7 'How is it going?'",
"project": "web",
"dir": "/path/to/web"
}
]Jun sets these in each of its terminals:
| Variable | Value |
|---|---|
JUN_API |
The socket of the Jun that runs the terminal. The commands try no other: without it they fail. |
JUN_BIN |
Jun's executable |
JUN_TERMINAL |
The terminal's id, for reference |
Jun tells which terminal a caller runs in by its processes, as macOS reports them, not by JUN_TERMINAL.
- The socket:
jun.sock, beside the configuration file. - Who can connect: only your user.
- Another Jun: while a Jun serves the socket, another one with the same configuration file serves no tools, and says so. A socket left by a Jun that ended is replaced.
- Outside Jun's terminals: the read-only commands work from anywhere with
JUN_APIset, for instance to debug, and thenselfisfalseeverywhere. Changes are refused (caller is not a terminal of this Jun). - A Jun in another's terminal: its terminals reach it, not the outer Jun. A program there that points
JUN_APIat the outer Jun by hand counts, for the outer Jun, as running in the terminal the inner Jun was started from, as its processes descend from that terminal's shell.
| Status | Meaning |
|---|---|
| 0 | Done |
| 1 | Refused, or Jun did not answer or cannot be reached; the reason goes to standard error |
| 2 | Usage error |
The commands connect to a Unix socket, which some agents' sandboxes block:
| Agent | Socket in its sandbox |
|---|---|
| Claude Code | Reached, with the sandbox's defaults |
| Codex | Blocked in read-only and workspace-write (connect: operation not permitted). Approve running the command outside the sandbox, or allow commands network access in ~/.codex/config.toml: network_access = true under [sandbox_workspace_write]. |
| Pi | Reached: Pi runs commands without a sandbox |
Checked with Claude Code 2.1.318, Codex 0.166.0 and Pi 1.5.0.
jun tool runs the tools. To open a directory named tool as a project, write jun ./tool.