An opt-in switch (/coordinator on|off) that keeps the main agent a coordinator: it may read, search and hire helpers, but file changes are left to them

A Claude Code mod that draws a live pixel-art office in the Claude desktop app. Every agent in the session is a character with a job title. Main is the coordinator: it talks with you, splits big jobs into parts and hires helpers for them. Helpers arrive by elevator, get briefed in the meeting room, walk to the station that matches the tool they are using, carry their report back to whoever hired them and leave. You can see who is waiting on whom, what each one was asked to do, and what each one found.
<img src="docs/images/office.svg" alt="The Pixel Office, a pixel-art floor plan. Main sits in the corner office with a speech bubble that reads waiting on 2. The Planner stands in the records library with a bubble reading session.ts. The Researcher walks across the floor carrying a yellow folder, its report, back to Main. In the meeting room the Assistant sits beside a new helper, Test fails, with a bubble reading waiting on Test fails, and the whiteboard reads find why a test fails." width="100%">
The picture above is the mod's own drawing of a made-up session, and it is animated. Main hired three helpers at once and is coordinating: its bubble says it is waiting on two of them. The Researcher has finished and is carrying its report to Main. The Assistant found a failing test and hired a helper of its own, so it is coordinating too, from the meeting room. To try it yourself, jump to Install.
<img src="docs/images/office-map.svg" alt="The same office with a coloured label on every zone: Corner office, Desks, Records library, Phone booths, Mailroom, Server room, Meeting room, Task board, Reception, Elevator and Break room." width="100%">
Each label names a zone. An agent walks to the zone that matches the tool it is using, so where a character stands tells you what it is doing. The same colours appear in the speech bubbles and in the legend under the picture.
| Zone | Stands for | What you see there |
|---|---|---|
| Corner office | Main's home base: thinking, or coordinating its helpers | Main sits here and goes back after every errand. Main's own edits and writes happen at this desk too. While it waits on helpers, its bubble says who: "waiting on Researcher", or "waiting on 3". |
| Desks | Edit, Write, NotebookEdit | Eight desks. Each helper gets its own and returns to it between jobs. |
| Records library | Read, Grep, Glob, LSP, Skill, ToolSearch | Looking things up: reading files and searching code. |
| Phone booths | WebFetch, WebSearch | Going out to the web. A booth light comes on while someone is on a call. |
| Mailroom | Outside services (mcp__ tools) | Calling connected services. A parcel appears in the pigeonholes while someone is there. |
| Server room | Bash, PowerShell, background shells | Running commands. The rack lights flash while a command runs. |
| Meeting room | Briefings, SendMessage | A new helper is briefed here, and the whiteboard shows what it was hired to do. When one agent messages another, the sender carries an envelope over to the recipient, or comes here if it cannot find them. |
| Task board | TodoWrite, Task tools, plan updates | A cork board on the wall. Green notes are done, amber notes are still open. |
| Reception | Waiting on you: questions, permissions, plan approval | A blinking "!" bubble means this agent cannot go on until you answer. |
| Elevator | Agents arrive and leave | Helpers step out of it when they are hired and step back in when they finish. |
| Break room | Idle agents, and Main between turns after a while | Where anyone with nothing to do goes to rest. |
Every helper is named for the job its kind of agent does, so you can tell what it can and cannot do at a glance. The name Claude Code uses for that kind of agent is in its details box.
| In the office | Claude Code calls it | What it does |
|---|---|---|
| Main, the Coordinator | the main agent | Talks with you, splits the work into jobs, hires helpers and puts their reports together. |
| Researcher | Explore | Reads and searches, then reports what it found. Changes nothing. |
| Planner | Plan | Works out a step-by-step plan for a job. Changes nothing. |
| Assistant | general-purpose | Can read, edit and run commands to finish one job. |
| Clone | fork | A copy of its boss that knows everything its boss knew, sent off to do a side job. |
| Guide | claude-code-guide | Looks up how Claude Code itself works. |
| Contractor | (unknown) | A helper the office did not see being hired. |
Any other kind of agent is named after its type: a code-reviewer is a "Code reviewer". A helper the engine gave a name keeps it ("Scout"), and its details box adds the role. A second helper of the same kind is named for its job, as in "Test fails" (a researcher hired to find why a test fails); with no job to go on, or the same job, it is numbered, as in "Researcher 2".
How they work together. A boss hires a helper in one of two ways, and the office shows which:
Helpers can hire helpers of their own, so a chain of delegation shows up as a deeper indent in the panel and as a second coordinator on the floor. Teammates (helpers started as part of a team) are standing colleagues rather than errands: nobody waits on them, and they rest in the break room between jobs.
A helper is a subagent: a short-lived agent that Main (or another agent) hires for one job. In the picture it goes through these steps.
auth.ts or Test the code. It sets off with the helper, so you see the task before it arrives, and the furniture it is using is outlined in the station's colour until it leaves. Helpers standing close together get their bubbles in rows, one above the other; where the top of the floor leaves no room for another row, that helper's bubble is left out and the panel still says what it is doing. The feed says it in words: "Researcher is reading auth.ts", "Assistant is running a command to run the test suite" (a command is shown by what it is for, not by how it starts. Claude usually describes a command when it runs one, and that description is used when it fits the bubble. Without one the office reads the command: it skips the setup in front (cd, variables, mkdir), follows a chain, a pipe or a heredoc to the first real step, and names it, such as "Open a pull request", "Write bar.ts" or "Search for sceneTimeOf". It knows git, GitHub, the package managers, scripts, file and text tools, the network, containers and PowerShell; a program it does not know is shown as typed. A to-do list update shows the item being worked on, a search shows where it looks, and a call to an outside service shows what it is about).Two signs worth knowing. A red puff above a character means a tool call just hit an error. A blinking "!" bubble at reception means that agent is waiting on you, for a question, a permission or a plan to approve.
<img src="docs/images/pane-preview.svg" alt="The Pixel Office pane. The office picture sits on top. Underneath, on the left, the feed What's happening lists the last few events with their times. On the right, Who's doing what lists Main as coordinating, waiting on the Planner and the Assistant; the Planner reading session.ts; the Assistant coordinating its own helper, Test fails, who is being briefed; and the Researcher, done, having handed in its report. A dim line under each helper gives its objective. A details box for the Researcher shows its role, its job, its brief, who it reports to, the start of its report, its tokens and how long it was on the job. A Show legend button and a Show this job button sit at the bottom." width="100%">
The numbers below match the numbers in the picture.
The office picture is drawn as one picture by default. There is an experimental layered mode, where the scenery is one picture and each agent is its own transparent picture stacked on top of it, so one agent doing something would never restart another agent's walk. A walker is left alone from the moment its walk starts until it arrives, and the cat is its own picture so nothing else restarts it. Its first version did not show the agents in the author's pane, because their pictures were laid out with no room. They are given the whole region now, but that has not been checked in a pane yet, so treat it as an experiment; set the picture option to layered to try it (see Commands and options).
There is a cat in the office. It has nothing to do with your session: it patrols the corridor, sits for a while at a few favourite spots, and naps on the break-room couch, on a loop of about a minute. It is drawn as animations inside the picture, so it costs no extra redraws, and it carries on from the same point of its loop whenever the picture is drawn again. It is left out only when the picture is too large to draw the decorations.
A job is everything from a message you send until the next one. A notice from a background helper or another session that wakes Main is not a new message, so it carries on the same job. Once you have sent one, a Show this job button sits beside the legend button. It opens a recap that answers the question delegation raises: did splitting the work up help? If Main edited files itself rather than through a helper, the recap counts those edits too, so a coordinator that is not delegating shows.
<img src="docs/images/job-recap.svg" alt="The recap of a finished job. This job: fix the login bug. Took 4 minutes 22 seconds, 4 helpers, up to 4 at once, 207 thousand tokens. Main made 1 file edit itself, not through a helper. Helpers did 8 minutes 28 seconds of work in 3 minutes 40 seconds: about 2.3 times faster than one after another. A timeline shows one bar per agent: Main across the whole job; the Researcher, Planner and Assistant starting together; Test fails, hired by the Assistant, starting later. Each helper's bar ends with a tick and how long it took, with its share of the tokens beside it. Under it, one line per agent gives its time, tokens and how its job ended." width="100%">
The recap only shows the job in front of you. To keep a short record of each job across sessions, set the history option to last-20 (see Commands and options): the recap then lists your earlier jobs too. It is off unless you switch it on.
The recap also shows a This session line: the jobs, helpers, time and tokens of every job since the conversation started or was cleared, with the tokens split between Main and its helpers. It is kept in memory only, unless the history option is on, in which case each saved job keeps its token split too. Each agent shows its share of the job's tokens next to its time bar. An agent that is still running shows "running" there, because the engine only reports its tokens when it finishes.
In a terminal session the pane shows the same feed and team as plain text, under a line telling you to open the session in the Claude desktop app to see the picture.
Option 1: Using only the Claude desktop app? The desktop app's Code tab has no /plugin install and usually no claude on your PATH, but the app bundles its own Claude Code CLI. Start a Code session in the app and paste this prompt:
Install the "Pixel Office" Claude Code mod for me. Use the Claude Code CLI bundled with this desktop app: on Windows it is under `%APPDATA%\Claude\claude-code\<version>\…\claude.exe`, on macOS under `~/Library/Application Support/Claude/claude-code/<version>/` (pick the newest version folder; use `claude` on PATH only if it is 2.1.295 or newer). Run `<that claude> --version` and confirm it is 2.1.295 or newer. Then run `<that claude> plugin marketplace add sb2403/pixel-office` and `<that claude> plugin install pixel-office@pixel-office --config autoOpen=session-start`, show me `<that claude> plugin list`, and tell me to start a new session.
Approve the commands when Claude asks. The --config autoOpen=session-start in that prompt makes the office open at the start of every session; to change that, see Commands and options below. If the bundled version is older than 2.1.295, update the Claude desktop app first. Then start a new session and the office opens.
Option 2: From a terminal.
Check your version. Run claude --version in a terminal. It must show 2.1.295 or newer. If it is older, run claude update first.
Install from a terminal. Run these two commands in PowerShell, Command Prompt, or the macOS/Linux Terminal, not in the Claude chat box:
claude plugin marketplace add sb2403/pixel-office
claude plugin install pixel-office@pixel-office
The same two steps also work inside a terminal Claude Code session, as /plugin marketplace add sb2403/pixel-office then /plugin install pixel-office@pixel-office.
The Claude desktop app's built-in plugin browser won't find this mod; use Option 1 or Option 2 above. Once installed, it loads in desktop sessions too.
Nothing asks you anything: the install goes ahead at user scope, which is the default, and then prints a note that options are not set yet. That is fine: each stays at its default. autoOpen, the option for when the office opens by itself, stays at on-first-subagent, and history stays off. To pick another value, add --config autoOpen=session-start (or never) to the install line, or set it later with /plugin configure pixel-office@pixel-office or the --values-stdin form under Commands and options below. Start a new session: the office opens the first time a subagent is hired, or any time with /office.
Fallback. If those commands do not work, download the repository from GitHub (Code → Download ZIP, then unzip it) or run git clone https://github.com/sb2403/pixel-office. Then run these two commands in a terminal, with the path set to the unzipped or cloned folder:
claude plugin marketplace add "<path-to-the-unzipped-folder>"
claude plugin install pixel-office@pixel-office
The quotes are needed if the path has spaces in it, and harmless if it does not.
/office shows or hides the office pane. Type it in any session, whatever autoOpen is set to. Its one-line reply is the only thing the mod ever adds to your conversation.
autoOpen decides when the office opens by itself:
on-first-subagent (default): the first time a subagent is hired.session-start: as soon as a session starts.never: only when you run /office.history decides whether the office keeps a record of your jobs across sessions:
off (default): nothing is kept once the session ends.last-20: the newest 20 jobs are kept on this computer, in the mod's own store: the start of the message that began each one (up to 120 characters), when it started, how long it took, how many helpers and how many at once, and its tokens. The recap lists them under "Earlier jobs".notify decides whether the office reminds you when an agent is waiting on you:
off (default): no reminders.after-30s: once an agent has waited 30 seconds for your answer, permission or plan approval, the office says so once, naming the agent ("Researcher is waiting for your permission"). It shows a toast over the conversation, and raises a notification through your notification setting. In a terminal that is a desktop notification or a bell, as your terminal does them; the Claude desktop app shows the toast, and only your own notification hooks run there. Claude Code may already notify you when it needs you; if the two double up, leave this off.picture decides how the office is drawn:
single (default): one picture for everybody. The cost is that every change to the picture starts the walks in it again, so walkers jump a little whenever the app takes longer or shorter to show one picture than it took for the one before.layered (experimental): each agent is its own picture stacked over the scenery, so one agent moving never restarts another one walking. Its first version drew the scenery but not the agents. That is fixed, but not yet checked in a pane, so go back to single if the agents do not show; with debug on, the log says how much room the agents' pictures got.debug is for troubleshooting (for example, walkers that jump), and is off by default:
off (default): nothing is recorded, and nothing extra runs.on: the mod keeps a log of what it does and writes it to pixel-office/debug/log.jsonl inside the plugin's folder, rewritten at most once a second and capped at the newest 3000 lines. It also draws two invisible one-cell probes in the pane (top and bottom) that report when the app mounts them, which happens again after the app unmounts and remounts them (for example when you scroll), and once a second the size of their region. With the layered picture there is a third probe, in a box laid out as an agent's picture is, so the log says how much room those pictures get: no rows means the app gave them none. Only event kinds, agent ids, tool names, counts, sizes and timings are logged: never commands, file names, labels, briefs, reports or any of your text. Send the file when asked to look into a problem, and set debug back to off afterwards.t (milliseconds) and kind: render (the pane was drawn, with its scroll and panel state), draw (a picture was drawn again, and why), layers (how many agent pictures a render had and how many were redrawn), event (something an agent did), flush (a batch of events applied, and how long it took), turn-start, turn-ignored (a helper's own turn, which is not a new task) and probe (what the pane reported back).Optional. To set it while installing, instead of answering the options screen, run this in place of the second install command (after claude plugin marketplace add sb2403/pixel-office):
claude plugin install pixel-office@pixel-office --config autoOpen=session-start
To change it later, run /plugin configure pixel-office@pixel-office inside Claude Code, or save the value from a terminal by piping a JSON object to configure:
echo '{"autoOpen":"never"}' | claude plugin configure pixel-office@pixel-office --values-stdin
That one-liner is for PowerShell and for macOS/Linux shells. In Command Prompt (cmd.exe) the single quotes would be passed through as part of the text, so use /plugin configure there, or run it from PowerShell.
Either way the new value applies to sessions you start afterwards. claude plugin configure pixel-office@pixel-office on its own only shows the options and whether each is set; it changes nothing.
Main is "idle" instead of "working"? Main is working from the moment you send a message until it finishes its turn. A working Main with no speech bubble is thinking between tools, in the corner office. When the turn ends Main reads "idle" and stays where it stood. A few s
hooks/register.ts 74 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { isExempt, writesFile } from './rules'
5
6// Off until it is switched on, for the session: `/coordinator on` makes Main hire helpers for file changes,
7// `/coordinator off` lets it edit again. Only Main is held to it: the main loop has no agentId, a helper's loop does.
8const enabledAtom = atom({ plugin: 'coordinator-guard', key: 'enabled' } as const, false)
9
10export const WHY =
11 'coordinator-guard: Main is the coordinator and does not change files itself while /coordinator is on. Hire a helper with the Agent tool, give it this job and its brief, and put its report together. (/coordinator off lets Main edit again.)'
12
13export const whyShell = (what: string): string =>
14 `coordinator-guard: this command writes a file (${what}), which is a helper's job while /coordinator is on. Hire a helper with the Agent tool for it. Main may still read, search, run tests and git, and write to its scratchpad. (/coordinator off lets Main write again.)`
15
16const HELP = 'Usage: /coordinator on | off | status'
17
18export const register: Register = on => {
19 on('session.start', async ($, e, next) => {
20 try {
21 await $.command.register({ name: 'coordinator', description: 'Keep Main a coordinator: on, off or status' })
22 } catch {
23 // The command is a convenience; the guard still works without it.
24 }
25 return next(e)
26 }).catch(($, e, next) => next(e))
27
28 on('command.run', { command: 'coordinator' }, async ($, e) => {
29 const word = (e.args ?? '').trim().toLowerCase()
30 const isOn = await read($, enabledAtom)
31 if (word === 'on' || word === 'off') {
32 const wants = word === 'on'
33 await update($, enabledAtom, () => wants)
34 return {
35 text: wants
36 ? 'Coordinator mode is on: Main will hire helpers instead of changing files itself.'
37 : 'Coordinator mode is off: Main may change files itself.',
38 }
39 }
40 if (word === '' || word === 'status') return { text: `Coordinator mode is ${isOn ? 'on' : 'off'}. ${HELP}` }
41 return { text: HELP }
42 })
43
44 // Editing tools: Main is refused anywhere but its own scratch places. A failure of the guard itself lets the call
45 // through: it is a convenience, not a lock.
46 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
47 if (e.agentId !== undefined || !(await read($, enabledAtom)) || isExempt(e.file_path)) return next(e)
48 return { deny: WHY }
49 }).catch(($, e, next) => next(e))
50
51 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
52 if (e.agentId !== undefined || !(await read($, enabledAtom)) || isExempt(e.file_path)) return next(e)
53 return { deny: WHY }
54 }).catch(($, e, next) => next(e))
55
56 on('tool.call', { tool: 'NotebookEdit' }, async ($, e, next) => {
57 if (e.agentId !== undefined || !(await read($, enabledAtom)) || isExempt(e.notebook_path)) return next(e)
58 return { deny: WHY }
59 }).catch(($, e, next) => next(e))
60
61 // Shell commands that write a file: a redirect, tee, an in-place edit, a PowerShell writer.
62 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
63 if (e.agentId !== undefined || !(await read($, enabledAtom))) return next(e)
64 const what = writesFile(e.command)
65 return what === null ? next(e) : { deny: whyShell(what) }
66 }).catch(($, e, next) => next(e))
67
68 on('tool.call', { tool: 'PowerShell' }, async ($, e, next) => {
69 if (e.agentId !== undefined || !(await read($, enabledAtom))) return next(e)
70 const what = writesFile(e.command)
71 return what === null ? next(e) : { deny: whyShell(what) }
72 }).catch(($, e, next) => next(e))
73}
74hooks/rules.ts 157 lines1// What the guard counts as Main doing a helper's job: changing a file itself.
2//
3// This is a nudge towards delegating, not a security boundary: it reads a command's text, so it cannot see what a
4// script does, and it does not resolve symbolic links.
5
6// Places Main may always write: its own memory and plans, and the temporary folder its scratchpad lives in.
7// Anything else is the work, and the work is for a helper. They are anchored to the home and temp folders, so a
8// `scratchpad` or `.claude/projects` folder inside a project is not exempt.
9const EXEMPT: readonly RegExp[] = [
10 /^(?:[A-Za-z]:)?\/(?:Users|home)\/[^/]+\/\.claude\/(?:projects|plans|dev-mods)\//,
11 /^\/root\/\.claude\/(?:projects|plans|dev-mods)\//,
12 /^(?:[A-Za-z]:)?\/Users\/[^/]+\/AppData\/Local\/Temp\/claude\//,
13 /^\/(?:private\/)?var\/folders\/[^/]+\/[^/]+\/T\//,
14 /^\/tmp\//,
15]
16
17// A path with backslashes, repeated slashes, `.` and `..` made plain; null when `..` climbs out of the root.
18export function normalize(path: string): string | null {
19 const parts: string[] = []
20 const raw = path.replace(/\\/g, '/').replace(/\/+/g, '/')
21 const drive = /^[A-Za-z]:/.exec(raw)?.[0] ?? ''
22 for (const part of raw.slice(drive.length).split('/')) {
23 if (part === '' || part === '.') continue
24 if (part === '..') {
25 if (parts.pop() === undefined) return null
26 continue
27 }
28 parts.push(part)
29 }
30 return `${drive}/${parts.join('/')}${raw.endsWith('/') && parts.length > 0 ? '/' : ''}`
31}
32
33export const isExempt = (path: string): boolean => {
34 const normal = normalize(path)
35 return normal !== null && EXEMPT.some(pattern => pattern.test(normal))
36}
37
38// The command with heredoc bodies taken out: `cat <<'EOF' > f\nbody\nEOF\necho x` becomes `cat > f\n\necho x`.
39// Only the body goes (the lines up to the line that is the delimiter): what is on the operator's line and what
40// follows the end of the body is still a command.
41export function withoutHeredocBodies(command: string): string {
42 let out = ''
43 let quote = ''
44 const pending: Array<{ delimiter: string; isTabbed: boolean }> = []
45 for (let i = 0; i < command.length; i++) {
46 const ch = command[i] as string
47 if (quote !== '') {
48 out += ch
49 if (ch === '\\' && quote === '"') {
50 out += command[i + 1] ?? ''
51 i++
52 } else if (ch === quote) quote = ''
53 continue
54 }
55 if (ch === '"' || ch === "'") {
56 quote = ch
57 out += ch
58 continue
59 }
60 // `<<<` is a here-string, one word: its last two characters are not a heredoc operator.
61 if (ch === '<' && command.startsWith('<<<', i)) {
62 out += '<<<'
63 i += 2
64 continue
65 }
66 if (ch === '<' && command[i + 1] === '<' && command[i + 2] !== '<') {
67 const found = /^<<(-?)\s*(?:'([^']*)'|"([^"]*)"|\\?([^\s;&|<>()]+))/.exec(command.slice(i))
68 if (found !== null) {
69 pending.push({ delimiter: found[2] ?? found[3] ?? found[4] ?? '', isTabbed: found[1] === '-' })
70 out += ' '
71 i += found[0].length - 1
72 continue
73 }
74 }
75 out += ch
76 if (ch === '\n' && pending.length > 0) {
77 // Skip the lines up to each pending delimiter, in order.
78 let at = i + 1
79 for (const { delimiter, isTabbed } of pending.splice(0)) {
80 for (;;) {
81 if (at >= command.length) break
82 const end = command.indexOf('\n', at)
83 const line = command.slice(at, end === -1 ? command.length : end)
84 at = end === -1 ? command.length : end + 1
85 if ((isTabbed ? line.replace(/^\t+/, '') : line) === delimiter) break
86 }
87 }
88 i = at - 1
89 }
90 }
91 return out
92}
93
94const NOWHERE = /^(\/dev\/null|nul|\$null)$/i
95
96// The files a shell command writes with a redirect: `> out`, `>> log`, `1> out`, `&> out`, `>| out`, found
97// outside quotes and outside heredoc bodies, not counting `2>&1`, `>&2` or a write to /dev/null.
98export function redirectTargets(command: string): string[] {
99 const text = withoutHeredocBodies(command)
100 const out: string[] = []
101 let quote = ''
102 for (let i = 0; i < text.length; i++) {
103 const ch = text[i] as string
104 if (quote !== '') {
105 if (ch === '\\' && quote === '"') i++
106 else if (ch === quote) quote = ''
107 continue
108 }
109 if (ch === '"' || ch === "'") {
110 quote = ch
111 continue
112 }
113 if (ch !== '>') continue
114 if (text[i + 1] === '&') {
115 i++
116 continue
117 }
118 let j = i + 1
119 if (text[j] === '>' || text[j] === '|') j++
120 while (text[j] === ' ' || text[j] === '\t') j++
121 let target = ''
122 let inQuote = ''
123 for (; j < text.length; j++) {
124 const c = text[j] as string
125 if (inQuote !== '') {
126 if (c === inQuote) inQuote = ''
127 else target += c
128 } else if (c === '"' || c === "'") inQuote = c
129 else if (/[\s;&|<>)]/.test(c)) break
130 else target += c
131 }
132 i = j - 1
133 if (target !== '' && !NOWHERE.test(target)) out.push(target)
134 }
135 return out
136}
137
138// Other ways a command writes a file, whatever its redirects: tee, an in-place edit, PowerShell's writers.
139const WRITERS: ReadonlyArray<readonly [RegExp, string]> = [
140 [/(^|[\s;&|(])tee(\s|$)/, 'tee'],
141 [/(^|[\s;&|(])(sed|perl|ruby)\s+(?:[^;&|]*\s)?-[A-Za-z]*i(\.\w+)?(\s|$)/, 'an in-place edit'],
142 [/(^|[\s;&|(])(Set-Content|Add-Content|Out-File|New-Item|Export-Csv|Export-Clixml)(\s|$)/i, 'a PowerShell file writer'],
143 [/(^|[\s;&|(])(git\s+apply|patch)(\s|$)/, 'a patch'],
144]
145
146// What in a command changes a file when Main is meant not to, or null when it does not: a redirect to a place Main
147// may not write, or one of the writers above. A redirect into an exempt place (the scratchpad) is allowed.
148export function writesFile(command: string): string | null {
149 const blocked = redirectTargets(command).find(target => !isExempt(target))
150 if (blocked !== undefined) return `a redirect to ${blocked}`
151 const text = withoutHeredocBodies(command).replace(/"[^"]*"|'[^']*'/g, '""')
152 for (const [pattern, name] of WRITERS) {
153 if (pattern.test(text)) return name
154 }
155 return null
156}
157types/index.d.ts 9 lines1// The one value the guard keeps for the session: whether Main is being held to coordinating.
2export type Switch = boolean
3
4declare module 'claude-code' {
5 interface PluginState {
6 'coordinator-guard': { enabled: boolean }
7 }
8}
9