@basein/runner 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,6 +79,8 @@ written; wraps an older install made there are put back. `--mcp-json` (or
79
79
  `BIR_WRAP_MCP_JSON=1`) is that old in-place shape, for a `.mcp.json` nobody
80
80
  commits. The hooks go into `.claude/settings.local.json` — your file, not the
81
81
  one a team commits; in a git repository it is added to `.git/info/exclude`.
82
+ `bir setup` also puts `mcp__bir` in that file's `permissions.allow`, so Claude
83
+ Code runs the `bir` server's tools without asking (a forced change still asks).
82
84
  Uninstall repairs `~/.claude.json` (and a `--config` file) entry by entry;
83
85
  `settings.local.json` it puts back byte-for-byte when nothing else has edited
84
86
  it since, and repairs entry by entry when something has. A project remembers
@@ -248,12 +250,18 @@ between the two is not guaranteed.
248
250
  `bir` server's `run_scenario` (only with `--replay`), and replay's own
249
251
  `[BaseInstRunner calculated replay]` notes when a prompt matches a scenario.
250
252
  The `bir` server's three tools that only read a scenario (`scenario_show`,
251
- `scenario_edits`, `investigate`) are listed only after `bir scenario editing on`,
252
- or with `BIR_SCENARIO_TOOLS=read` in its environment: their definitions ride
253
- along with every request. The three for changing one (`scenario_check`,
254
- `scenario_edit`, `scenario_undo`) appear only after `bir scenario editing on`
255
- in that project (not yet released, see below). With the runner off it lists
256
- no tools at all.
253
+ `scenario_edits`, `investigate`) and the three for changing one
254
+ (`scenario_check`, `scenario_edit`, `scenario_undo`) come with scenario
255
+ editing, which `bir setup` turns on. Setup also lets Claude Code run every
256
+ `bir` tool without asking: it adds `mcp__bir` to `permissions.allow` in
257
+ `.claude/settings.local.json`, and uninstall takes it out again. A change
258
+ forced past the scenario check (`force`) still asks the person first. Their
259
+ definitions ride along with every request, so `bir scenario editing off`
260
+ takes the six away, and a later setup leaves that off. A bare
261
+ `bir install --replay` lists `run_scenario` alone until
262
+ `bir scenario editing on` (or `BIR_SCENARIO_TOOLS=read` in the server's
263
+ environment, for the reading three). With the runner off it lists no tools
264
+ at all.
257
265
  - **Record its own `bir` calls** (next version, not yet released). A session
258
266
  that fixes a plan is not a task to replay. The `bir` server's tools other
259
267
  than `run_scenario`, and a `Bash` or `PowerShell` command made only of `bir`
@@ -331,9 +339,9 @@ copy — or on purpose, with `--force --note "why"`.** Only the owner can change
331
339
  a scenario. `bir scenario undo` takes changes back one at a time: run it again
332
340
  to go one further back. An undo is checked too, and is refused if it would
333
341
  break a later step or the final answer, unless you add `--force --note "why"`.
334
- Claude can do all of this: in its terminal, or with the `bir` server's tools
335
- once `bir scenario editing on` was run in the project. Step by step, with real
336
- output:
342
+ Claude can do all of this: in its terminal, or with the `bir` server's tools,
343
+ which `bir setup` turns on (`bir scenario editing on` in a project wired by
344
+ `bir install`). Step by step, with real output:
337
345
  [the guide's §9.2](docs/calculatedReplayGuide.md).
338
346
 
339
347
  ```bash
@@ -344,7 +352,7 @@ bir scenario edits scn_… # the history, newe
344
352
  bir scenario undo scn_… # take back the newest edit; again to go further back
345
353
  bir scenario undo scn_… --step 3 # take back step 3's newest edit only
346
354
  bir scenario calc scn_… --force --discard-edits # recalculate this plan, dropping its hand edits
347
- bir scenario editing on # let Claude's `bir` tools change scenarios here
355
+ bir scenario editing on # let Claude's `bir` tools change scenarios here (setup did)
348
356
  ```
349
357
 
350
358
  **A value no prompt names.** Built 2026-09-28, not yet released. Every
@@ -439,7 +447,7 @@ what to do when they register, so there is one switch and no second copy of it:
439
447
  | `BIR_REPLAY_RECORDED_OUTPUTS=1` | Let a step's recorded output stand in when the step could not be sent. Unset, the plan stops at that step and hands the task back. |
440
448
 
441
449
  The `bir` server reads its own: `BIR_SCENARIO_TOOLS=read` lists its three reading
442
- tools without `bir scenario editing on`, and `BIR_SCENARIO_RUN_TIMEOUT_MS` is how
450
+ tools where scenario editing is off, and `BIR_SCENARIO_RUN_TIMEOUT_MS` is how
443
451
  long `run_scenario` waits for a run (default `290000`). Everything else — remote
444
452
  upstreams, the recorder's backlog, service timeouts, the `bir` server's output
445
453
  caps — is in [production.md](production.md).
@@ -61,14 +61,15 @@
61
61
  * WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
62
62
  * switched off (`bir off`), when nothing is: an inert runner adds nothing to
63
63
  * the model's context, and a call from a list taken before the switch starts
64
- * nothing and asks nobody. The reading tools come with `bir scenario editing on`
65
- * (or `BIR_SCENARIO_TOOLS=read` in this server's environment): their
66
- * definitions ride along with every request of every session, and only
67
- * somebody working on a scenario ever calls them. The tools that CHANGE a
68
- * scenario are offered only where a person ran `bir scenario editing on` (D2):
69
- * this server runs in every session of every installed project, a fleet
70
- * included, and a plan must not change because some agent there decided it
71
- * should. The switch is read when the host lists the tools, and again on every
64
+ * nothing and asks nobody. The rest come with scenario editing, which
65
+ * `bir setup` turns on (a person's `bir scenario editing off` is kept): the
66
+ * reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
67
+ * environment — and the tools that CHANGE a scenario (D2). A project wired by
68
+ * a bare `bir install` (a fleet, say) offers `run_scenario` alone until a
69
+ * person runs `bir scenario editing on`: this server runs in every session
70
+ * there, each listed tool's definition rides along with every request, and a
71
+ * plan must not change because some agent there decided it should. The
72
+ * switch is read when the host lists the tools, and again on every
72
73
  * call, so a call to a tool that was never offered — or was offered before the
73
74
  * switch went off — is refused rather than run. Turning the runner off or on
74
75
  * mid-session tells the host its list changed (`notifications/tools/list_changed`).
@@ -61,14 +61,15 @@
61
61
  * WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
62
62
  * switched off (`bir off`), when nothing is: an inert runner adds nothing to
63
63
  * the model's context, and a call from a list taken before the switch starts
64
- * nothing and asks nobody. The reading tools come with `bir scenario editing on`
65
- * (or `BIR_SCENARIO_TOOLS=read` in this server's environment): their
66
- * definitions ride along with every request of every session, and only
67
- * somebody working on a scenario ever calls them. The tools that CHANGE a
68
- * scenario are offered only where a person ran `bir scenario editing on` (D2):
69
- * this server runs in every session of every installed project, a fleet
70
- * included, and a plan must not change because some agent there decided it
71
- * should. The switch is read when the host lists the tools, and again on every
64
+ * nothing and asks nobody. The rest come with scenario editing, which
65
+ * `bir setup` turns on (a person's `bir scenario editing off` is kept): the
66
+ * reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
67
+ * environment — and the tools that CHANGE a scenario (D2). A project wired by
68
+ * a bare `bir install` (a fleet, say) offers `run_scenario` alone until a
69
+ * person runs `bir scenario editing on`: this server runs in every session
70
+ * there, each listed tool's definition rides along with every request, and a
71
+ * plan must not change because some agent there decided it should. The
72
+ * switch is read when the host lists the tools, and again on every
72
73
  * call, so a call to a tool that was never offered — or was offered before the
73
74
  * switch went off — is refused rather than run. Turning the runner off or on
74
75
  * mid-session tells the host its list changed (`notifications/tools/list_changed`).
package/dist/bin/bir.js CHANGED
@@ -46,14 +46,14 @@ import * as daemonApi from "../control/daemon.js";
46
46
  import { activeSessions, daemonStatus, ensureDaemon, probeHealth, readPortProblem, stopDaemon, } from "../control/daemon.js";
47
47
  import { configDir, controlDir, controlKey, discoveryPath, installedPath, normalizePath } from "../control/paths.js";
48
48
  import { offFlagPath, offSource, projectOffFlagPath, setRunnerOff } from "../util/off-switch.js";
49
- import { allocateFreeProjectPort, backupIsCreation, backupOriginalText, configSha, envProxyHint, exemptLoopbackFromProxy, isNpxEntry, isScenarioServer, isWrapped, nodeFlagsToCarry, normaliseRecordKeys, npxHookInvocation, protectBackupText, recordKey, sameServerConfig, PACKAGE_NAME, projectRecord, readSidecar, redactSecrets, restoreSecrets, remoteWrapBlocker, scenarioEntry, setProjectRecord, settleReplaySwitch, sidecarKey, unwrapEntry, UPSTREAM_HEADERS_ENV, wrapEntry, writeSidecar, SCENARIO_SERVER_KEY, } from "../config/generate.js";
49
+ import { allocateFreeProjectPort, backupIsCreation, backupOriginalText, configSha, envProxyHint, exemptLoopbackFromProxy, isNpxEntry, isScenarioServer, isWrapped, nodeFlagsToCarry, normaliseRecordKeys, npxHookInvocation, protectBackupText, recordKey, sameServerConfig, PACKAGE_NAME, projectRecord, readSidecar, redactSecrets, restoreSecrets, remoteWrapBlocker, scenarioEntry, editingEnabled, setProjectRecord, settleEditing, settleReplaySwitch, sidecarKey, unwrapEntry, UPSTREAM_HEADERS_ENV, wrapEntry, writeSidecar, SCENARIO_SERVER_KEY, } from "../config/generate.js";
50
50
  import { chooseNodePath } from "../config/node-path.js";
51
- import { managedMcpPolicyBlocker, mcpjsonApprovals } from "../config/mcp-approval.js";
51
+ import { claudeUserSettingsPath, managedMcpPolicyBlocker, mcpjsonApprovals } from "../config/mcp-approval.js";
52
52
  import { checkOverrides, recordServerName } from "../config/overrides.js";
53
53
  import { awaitWithin, isUnhealthyStatus, serviceHealth, serviceTimeoutMs, timeoutSignal } from "../util/service-health.js";
54
54
  import { runSetup } from "./setup.js";
55
55
  import { claudeConfigHome, claudeProjectKey, claudeProjectRoot, isRemote, linkedWorktrees, parseJsonFile, projectKeyFor, projectKeysFor, projectScope, resolveServers, } from "../config/resolve.js";
56
- import { assertEditableJson, buildHooksBlock, claudeCodePaths, disableMcpjsonServers, enableMcpjsonServers, ENSURE_ARG, ensureGitExcludeLine, hasBirHooks, installHooks, isBirHook, LOCAL_SETTINGS_EXCLUDE, readTextOrNull, removeGitExclude, setLocalEntries, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
56
+ import { addAllowRules, allowRules, assertEditableJson, BIR_TOOLS_RULE, buildHooksBlock, claudeCodePaths, disableMcpjsonServers, enableMcpjsonServers, ENSURE_ARG, ensureGitExcludeLine, hasBirHooks, installHooks, isBirHook, LOCAL_SETTINGS_EXCLUDE, readTextOrNull, removeAllowRules, removeGitExclude, setLocalEntries, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
57
57
  import { writeFileAtomic } from "../util/atomic-write.js";
58
58
  import { readGenericServers, setGenericServerEntry } from "../config/adapters/generic.js";
59
59
  import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, clearCredentials, deviceLogin, describeAuthService, legacyPasswordLogin, tokenLogin, normalizeAuthUrl, rememberAuthUrl, resolveAuthUrl, } from "../auth/client.js";
@@ -80,7 +80,8 @@ function usage(code = 0) {
80
80
  out(`bir ${VERSION} — a recording MCP proxy
81
81
 
82
82
  Commands:
83
- setup the one command: sign in, wire this project, start the recorder
83
+ setup the one command: sign in, wire this project, start the recorder, and give
84
+ Claude Code every bir tool (run_scenario, investigate, scenario_*) with no prompt
84
85
  up start the recorder for this project in the background (--restart: replace it)
85
86
  down stop it (open sessions here then show a hook error until they restart;
86
87
  \`bir off --here\` is the quiet way to take it out of the way)
@@ -119,7 +120,8 @@ Commands:
119
120
  take back the newest edit (of step n, or parameter name);
120
121
  run it again to go one further back; undo an undo's id to redo
121
122
  scenario editing on|off|status whether this project's bir MCP server offers the tools
122
- that change a scenario (restart Claude Code after)
123
+ that read and change a scenario (restart Claude Code after);
124
+ \`bir setup\` turns it on, and an off stays off
123
125
  scenario replay <scnId> --prompt "…" [--dry]
124
126
  replay --scenario <scnId> --prompt "…" [--dry]
125
127
  replay status | on | off | allow <a,b> | allow all
@@ -770,6 +772,15 @@ async function installWriting(args, cwd, sidecar, plan) {
770
772
  ? " - calculated replay is off here (--no-replay); `bir replay on` turns it on"
771
773
  : " - calculated replay is off here: it was not asked for (`bir install --replay` adds it)");
772
774
  }
775
+ // EVERY `bir` TOOL, for `bir setup`: investigate and the scenario tools are
776
+ // offered as well as run_scenario (scenario editing on — an `off` a person
777
+ // stored stays off), and Claude Code runs them without asking (below, with
778
+ // the hooks). Only where `bir` is the scenario server: a `bir` server of the
779
+ // person's own gets neither.
780
+ const birToolsHere = args.birTools === true && scenarioServerHere(args, cwd);
781
+ if (birToolsHere && settleEditing(sidecar, cwd)) {
782
+ out(" + scenario editing on: the bir server offers investigate and the scenario tools too (`bir scenario editing off` takes them away)");
783
+ }
773
784
  // SessionStart starts the recorder itself when this directory has none
774
785
  // (control/ensure-hook.ts). Every invocation gets it — an npx one through
775
786
  // npx, once per session — because without it nothing starts the recorder,
@@ -814,9 +825,35 @@ async function installWriting(args, cwd, sidecar, plan) {
814
825
  // never asks about; an older install's pre-approval has nothing to approve.
815
826
  text = disableMcpjsonServers(paths.settingsLocal, [SCENARIO_SERVER_KEY]) ?? text;
816
827
  }
828
+ let allowLine;
829
+ if (args.birTools) {
830
+ // A prompt for a tool the person never named reads as the runner getting
831
+ // in the way, and under `claude -p` it is a refusal. `run_scenario` runs
832
+ // only a plan the recorder armed, the reading tools only read, and a
833
+ // change must reproduce the recording or be saved on purpose with a note
834
+ // (editSteps.md D3) — and `scenario_undo` takes it back.
835
+ const granted = projectRecord(sidecar, cwd)?.allowRules ?? [];
836
+ if (birToolsHere) {
837
+ const allowed = addAllowRules(paths.settingsLocal, [BIR_TOOLS_RULE]);
838
+ text = allowed.text;
839
+ if (allowed.added.length > 0) {
840
+ setProjectRecord(sidecar, cwd, { port, allowRules: [...new Set([...granted, ...allowed.added])] });
841
+ allowLine = ` + ${BIR_TOOLS_RULE} → permissions.allow in the same file (Claude Code runs the bir tools without asking)`;
842
+ }
843
+ }
844
+ else if (granted.length > 0) {
845
+ // `bir` here is not the scenario server any more: the rule an earlier
846
+ // setup added must not approve another server's tools by its name.
847
+ text = removeAllowRules(paths.settingsLocal, granted) ?? text;
848
+ setProjectRecord(sidecar, cwd, { port, allowRules: undefined });
849
+ allowLine = ` - ${granted.join(", ")} — taken out of permissions.allow there: \`${SCENARIO_SERVER_KEY}\` here is not the scenario server`;
850
+ }
851
+ }
817
852
  noteWritten(sidecar, paths.settingsLocal, text, claudeJson);
818
853
  sidecar.hookFiles = [...new Set([...(sidecar.hookFiles ?? []), paths.settingsLocal])];
819
854
  out(` + hooks → ${paths.settingsLocal} (control server on ${controlUrl}; the recorder starts itself with each session${invocation === "npx" ? ", through npx" : ""})`);
855
+ if (allowLine)
856
+ out(allowLine);
820
857
  const exclude = ensureGitExcludeLine(cwd);
821
858
  if (exclude) {
822
859
  out(` + ${LOCAL_SETTINGS_EXCLUDE} is listed in ${exclude.path}`);
@@ -1847,6 +1884,22 @@ function uninstallProject(cwd, sidecar, opts) {
1847
1884
  }
1848
1885
  }
1849
1886
  }
1887
+ // 4b. The rule `bir setup` added so the `bir` server's tools run without a
1888
+ // prompt: only the rules it added, and once that server is gone — so a `bir`
1889
+ // server of the person's own never inherits it. `--replay` takes it too.
1890
+ const projectRec = projectRecord(sidecar, cwd);
1891
+ const granted = projectRec?.allowRules ?? [];
1892
+ if (projectRec && granted.length > 0 && wanted(SCENARIO_SERVER_KEY) && !remaining.has(SCENARIO_SERVER_KEY)) {
1893
+ try {
1894
+ if (removeAllowRules(paths.settingsLocal, granted) !== undefined) {
1895
+ out(` ← ${granted.join(", ")} — taken out of ${paths.settingsLocal} (permissions.allow)`);
1896
+ }
1897
+ setProjectRecord(sidecar, cwd, { port: projectRec.port, allowRules: undefined });
1898
+ }
1899
+ catch (err) {
1900
+ remaining.set(`allow:${normalizePath(paths.settingsLocal)}`, `${granted.join(", ")} in ${paths.settingsLocal} — ${errText(err)}`);
1901
+ }
1902
+ }
1850
1903
  // 5. Read it back: what would a session here spawn now — in this
1851
1904
  // environment, and in any other profile the install wrote to?
1852
1905
  const after = runnerLeftovers(cwd, sidecar, opts.configPath, otherClaude);
@@ -3163,6 +3216,18 @@ async function doctor(args) {
3163
3216
  notes.push("the bir scenario server starts via npx, which can take longer than the first tool call " +
3164
3217
  "of a turn — run `bir setup` (or `bir install --replay --global`) to pin it to the installed copy");
3165
3218
  }
3219
+ // WHICH `bir` TOOLS A SESSION HERE IS GIVEN. `bir setup` offers every one and
3220
+ // lets Claude Code run them without asking; said only when that is not so.
3221
+ if (scenarioEntry && isScenarioServer(scenarioEntry.config)) {
3222
+ if (!editingEnabled(sidecar, cwd)) {
3223
+ notes.push("the bir MCP server offers run_scenario only: scenario editing is off here — `bir scenario editing on` " +
3224
+ "adds investigate and the scenario tools (restart Claude Code after)");
3225
+ }
3226
+ else if (![claudeUserSettingsPath(), paths.settings, paths.settingsLocal].some((file) => allowRules(file).some((rule) => rule === BIR_TOOLS_RULE || rule === `${BIR_TOOLS_RULE}__*`))) {
3227
+ notes.push("Claude Code asks before investigate and the scenario tools run here — `bir setup` allows every bir tool " +
3228
+ `(${BIR_TOOLS_RULE} in ${paths.settingsLocal})`);
3229
+ }
3230
+ }
3166
3231
  // THE PORT. The hooks' own URLs are the ground truth — they are what every
3167
3232
  // hook posts to — checked against the recorder actually answering here. Not
3168
3233
  // the machine-wide "last port any install wrote", which named another
@@ -3761,6 +3826,7 @@ async function main() {
3761
3826
  npx: false,
3762
3827
  forceInvocation: invocation,
3763
3828
  replay,
3829
+ birTools: true,
3764
3830
  hooks: true,
3765
3831
  servers: [],
3766
3832
  }),
@@ -10,7 +10,11 @@
10
10
  * 2. the sign-in — a setup token from the console, a cached session, or the
11
11
  * browser flow — stored the way `bir login` stores it;
12
12
  * 3. the project: MCP servers wrapped, hooks wired, the scenario server added
13
- * and pre-approved (`bir install --global --replay`);
13
+ * and pre-approved (`bir install --global --replay`) — and every one of
14
+ * its tools with it: investigate and the scenario tools are offered as
15
+ * well as run_scenario, and Claude Code runs them without asking (Eran,
16
+ * 2026-09-28: after setup, every `bir` tool works). A person's
17
+ * `bir scenario editing off` is kept;
14
18
  * 4. the recorder, started in the background and from now on started by the
15
19
  * SessionStart hook whenever it is missing;
16
20
  * 5. a summary that names the account, the servers, the recorder's log, and
package/dist/bin/setup.js CHANGED
@@ -10,7 +10,11 @@
10
10
  * 2. the sign-in — a setup token from the console, a cached session, or the
11
11
  * browser flow — stored the way `bir login` stores it;
12
12
  * 3. the project: MCP servers wrapped, hooks wired, the scenario server added
13
- * and pre-approved (`bir install --global --replay`);
13
+ * and pre-approved (`bir install --global --replay`) — and every one of
14
+ * its tools with it: investigate and the scenario tools are offered as
15
+ * well as run_scenario, and Claude Code runs them without asking (Eran,
16
+ * 2026-09-28: after setup, every `bir` tool works). A person's
17
+ * `bir scenario editing off` is kept;
14
18
  * 4. the recorder, started in the background and from now on started by the
15
19
  * SessionStart hook whenever it is missing;
16
20
  * 5. a summary that names the account, the servers, the recorder's log, and
@@ -35,8 +39,10 @@ import { pidAlive, readDiscovery } from "../control/discovery.js";
35
39
  import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, describeAuthService, deviceLogin, loadStoredConfig, normalizeAuthUrl, rememberAuthUrl, tokenLogin, } from "../auth/client.js";
36
40
  import { activeSessions, compareVersions, ensureDaemon, SETUP_TOKEN_ENV, stopDaemon, } from "../control/daemon.js";
37
41
  import { normalizePath } from "../control/paths.js";
38
- import { allocateReplacementPort, isScenarioServer, isWrapped, LOOPBACK_NO_PROXY, probeFreePort, proxyConfigured, projectRecord, readSidecar, SCENARIO_SERVER_KEY, setProjectRecord, writeSidecar, allocateFreeProjectPort, } from "../config/generate.js";
42
+ import { allowRules, BIR_TOOLS_RULE, claudeCodePaths } from "../config/adapters/claude-code.js";
43
+ import { allocateReplacementPort, editingEnabled, isScenarioServer, isWrapped, LOOPBACK_NO_PROXY, probeFreePort, proxyConfigured, projectRecord, readSidecar, SCENARIO_SERVER_KEY, setProjectRecord, writeSidecar, allocateFreeProjectPort, } from "../config/generate.js";
39
44
  import { resolveServers } from "../config/resolve.js";
45
+ import { EDIT_TOOLS, READ_TOOLS } from "./scenario-edit.js";
40
46
  import { errText } from "../util/log.js";
41
47
  import { offSource } from "../util/off-switch.js";
42
48
  import { packageVersion } from "../util/version.js";
@@ -368,6 +374,14 @@ export async function runSetup(args, deps) {
368
374
  else {
369
375
  out(` replay on — direct execution allowed for: ${allow?.length ? allow.join(", ") : `all wrapped (narrow it with \`bir replay allow ${wrapped[0]}\`)`}`);
370
376
  }
377
+ // What the `bir` server gives a session here, and whether Claude Code asks
378
+ // before each call — both set by install for setup (bir.ts, installWriting).
379
+ if (resolveServers(project).some((s) => s.name === SCENARIO_SERVER_KEY && isScenarioServer(s.config))) {
380
+ const asks = !allowRules(claudeCodePaths(project).settingsLocal).includes(BIR_TOOLS_RULE);
381
+ out(editingEnabled(readSidecar(), project)
382
+ ? ` tools ${["run_scenario", ...READ_TOOLS, ...EDIT_TOOLS].join(", ")} — ${asks ? "Claude Code asks before each" : "Claude Code runs them without asking, except a forced change"}`
383
+ : " tools run_scenario only — scenario editing is off here (`bir scenario editing on` adds investigate and the scenario tools)");
384
+ }
371
385
  if (ensured) {
372
386
  const { info, health: h } = ensured.status;
373
387
  const theirs = String(h.hooksVersion ?? info.version ?? "");
@@ -77,6 +77,33 @@ export declare function removeGitExclude(dir: string, line?: string): string | u
77
77
  export declare function enableMcpjsonServers(settingsPath: string, names: string[]): string;
78
78
  /** Undo {@link enableMcpjsonServers} for `names`; drops the key when empty. */
79
79
  export declare function disableMcpjsonServers(settingsPath: string, names: string[]): string | undefined;
80
+ /**
81
+ * The rule that lets Claude Code run the runner's own `bir` server's tools
82
+ * without asking. In Claude Code's permission syntax `mcp__<server>` matches
83
+ * every tool that server offers, so it also covers the tools `bir scenario
84
+ * editing on` brings, and any a later runner adds.
85
+ *
86
+ * `bir setup` writes it (bir.ts, installWriting), into the person's own
87
+ * `.claude/settings.local.json` only, and only where `bir` IS the runner's
88
+ * scenario server: the rule matches a server by name, and a server of the
89
+ * person's own that is called `bir` must keep its prompts.
90
+ */
91
+ export declare const BIR_TOOLS_RULE = "mcp__bir";
92
+ /** The `permissions.allow` rules in a settings file; none when it is missing or unreadable. */
93
+ export declare function allowRules(settingsPath: string): string[];
94
+ /**
95
+ * Add `rules` to `permissions.allow`, leaving every other rule and key alone.
96
+ * Returns the file's new text and the rules that were not there before — the
97
+ * only ones {@link removeAllowRules} may take out again. A `permissions` or
98
+ * `allow` of the wrong shape is left as it is (nothing added): it is the
99
+ * person's to fix, and Claude Code reads neither.
100
+ */
101
+ export declare function addAllowRules(settingsPath: string, rules: string[]): {
102
+ text: string;
103
+ added: string[];
104
+ };
105
+ /** Undo {@link addAllowRules} for `rules`; drops `allow`, then `permissions`, when left empty. */
106
+ export declare function removeAllowRules(settingsPath: string, rules: string[]): string | undefined;
80
107
  /** True when `settingsPath` holds any hook of ours. */
81
108
  export declare function hasBirHooks(settingsPath: string): boolean;
82
109
  /** Which file holds a given scope's `mcpServers` for this cwd. */
@@ -24,7 +24,7 @@ import { createHash } from "node:crypto";
24
24
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { dirname, join, parse as parsePath, resolve as resolvePath } from "node:path";
26
26
  import { writeFileAtomic } from "../../util/atomic-write.js";
27
- import { FIRST_PROJECT_PORT, PACKAGE_NAME } from "../generate.js";
27
+ import { FIRST_PROJECT_PORT, PACKAGE_NAME, SCENARIO_SERVER_KEY } from "../generate.js";
28
28
  import { claudeJsonPath, parseJsonFile, parseJsonText, projectKeyFor, } from "../resolve.js";
29
29
  export function claudeCodePaths(cwd, claudeJsonOverride) {
30
30
  return {
@@ -176,6 +176,74 @@ export function disableMcpjsonServers(settingsPath, names) {
176
176
  fresh.enabledMcpjsonServers = kept;
177
177
  });
178
178
  }
179
+ // ── permission rules ───────────────────────────────────────────────────────
180
+ /**
181
+ * The rule that lets Claude Code run the runner's own `bir` server's tools
182
+ * without asking. In Claude Code's permission syntax `mcp__<server>` matches
183
+ * every tool that server offers, so it also covers the tools `bir scenario
184
+ * editing on` brings, and any a later runner adds.
185
+ *
186
+ * `bir setup` writes it (bir.ts, installWriting), into the person's own
187
+ * `.claude/settings.local.json` only, and only where `bir` IS the runner's
188
+ * scenario server: the rule matches a server by name, and a server of the
189
+ * person's own that is called `bir` must keep its prompts.
190
+ */
191
+ export const BIR_TOOLS_RULE = `mcp__${SCENARIO_SERVER_KEY}`;
192
+ /** The `permissions.allow` rules in a settings file; none when it is missing or unreadable. */
193
+ export function allowRules(settingsPath) {
194
+ let parsed;
195
+ try {
196
+ parsed = parseJsonFile(settingsPath);
197
+ }
198
+ catch {
199
+ return [];
200
+ }
201
+ const allow = parsed?.permissions?.allow;
202
+ return Array.isArray(allow) ? allow.filter((r) => typeof r === "string") : [];
203
+ }
204
+ const plainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
205
+ /**
206
+ * Add `rules` to `permissions.allow`, leaving every other rule and key alone.
207
+ * Returns the file's new text and the rules that were not there before — the
208
+ * only ones {@link removeAllowRules} may take out again. A `permissions` or
209
+ * `allow` of the wrong shape is left as it is (nothing added): it is the
210
+ * person's to fix, and Claude Code reads neither.
211
+ */
212
+ export function addAllowRules(settingsPath, rules) {
213
+ let added = [];
214
+ const text = editJson(settingsPath, (parsed) => {
215
+ added = [];
216
+ if (parsed.permissions !== undefined && !plainObject(parsed.permissions))
217
+ return;
218
+ const permissions = { ...(parsed.permissions ?? {}) };
219
+ if (permissions.allow !== undefined && !Array.isArray(permissions.allow))
220
+ return;
221
+ const allow = [...(permissions.allow ?? [])];
222
+ added = rules.filter((rule) => !allow.includes(rule));
223
+ if (added.length === 0)
224
+ return;
225
+ permissions.allow = [...allow, ...added];
226
+ parsed.permissions = permissions;
227
+ });
228
+ return { text, added };
229
+ }
230
+ /** Undo {@link addAllowRules} for `rules`; drops `allow`, then `permissions`, when left empty. */
231
+ export function removeAllowRules(settingsPath, rules) {
232
+ if (!allowRules(settingsPath).some((rule) => rules.includes(rule)))
233
+ return undefined;
234
+ return editJson(settingsPath, (parsed) => {
235
+ const permissions = parsed.permissions;
236
+ if (!plainObject(permissions) || !Array.isArray(permissions.allow))
237
+ return;
238
+ const kept = permissions.allow.filter((rule) => !(typeof rule === "string" && rules.includes(rule)));
239
+ if (kept.length > 0)
240
+ permissions.allow = kept;
241
+ else
242
+ delete permissions.allow;
243
+ if (Object.keys(permissions).length === 0)
244
+ delete parsed.permissions;
245
+ });
246
+ }
179
247
  /** True when `settingsPath` holds any hook of ours. */
180
248
  export function hasBirHooks(settingsPath) {
181
249
  let parsed;
@@ -455,12 +455,23 @@ export interface ProjectRecord {
455
455
  * environment: their definitions ride along with every request of every
456
456
  * session, and only somebody working on a scenario calls them.
457
457
  *
458
- * Per project, and off by default, because the `bir` server runs in every
459
- * session of every installed project — a fleet included — and a plan must
460
- * not change because some agent there decided it should. The `bir scenario`
461
- * commands ignore it: a person in a terminal is asked before each one.
458
+ * Per project. `bir setup` turns it on (Eran, 2026-09-28: setup leaves every
459
+ * `bir` tool offered and working), except where a person turned it off —
460
+ * that `false` is kept by every later setup (see {@link settleEditing}). A
461
+ * bare `bir install` leaves it as it is, so a fleet wired by hand keeps the
462
+ * old default: the `bir` server runs in every session of every installed
463
+ * project, and there a plan must not change because some agent decided it
464
+ * should. The `bir scenario` commands ignore it: a person in a terminal is
465
+ * asked before each one.
462
466
  */
463
467
  editing?: boolean;
468
+ /**
469
+ * The `permissions.allow` rules `bir setup` added to this project's
470
+ * `.claude/settings.local.json` so Claude Code runs the `bir` server's tools
471
+ * without asking (adapters/claude-code.ts, BIR_TOOLS_RULE). Uninstall takes
472
+ * out only these: a rule that was there before is the person's own.
473
+ */
474
+ allowRules?: string[];
464
475
  /**
465
476
  * Every `.claude.json` an install for this project wrote local-scope entries
466
477
  * to. Claude Code reads `$CLAUDE_CONFIG_DIR/.claude.json` when that is set,
@@ -588,6 +599,12 @@ export declare function editingEnabled(sidecar: InstalledSidecar, cwd: string):
588
599
  * inventing a record would invent a port the hooks do not use.
589
600
  */
590
601
  export declare function setEditing(sidecar: InstalledSidecar, cwd: string, on: boolean): ProjectRecord | undefined;
602
+ /**
603
+ * `bir setup`: scenario editing on for `cwd`, unless it was ever set — an
604
+ * `off` a person stored is theirs, and no setup turns it back on. Returns true
605
+ * when it was switched on now.
606
+ */
607
+ export declare function settleEditing(sidecar: InstalledSidecar, cwd: string): boolean;
591
608
  /**
592
609
  * What an install or uninstall said about calculated replay:
593
610
  *
@@ -823,6 +823,18 @@ export function setEditing(sidecar, cwd, on) {
823
823
  return undefined;
824
824
  return setProjectRecord(sidecar, cwd, { port: record.port, editing: on });
825
825
  }
826
+ /**
827
+ * `bir setup`: scenario editing on for `cwd`, unless it was ever set — an
828
+ * `off` a person stored is theirs, and no setup turns it back on. Returns true
829
+ * when it was switched on now.
830
+ */
831
+ export function settleEditing(sidecar, cwd) {
832
+ const record = projectRecord(sidecar, cwd);
833
+ if (!record || record.editing !== undefined)
834
+ return false;
835
+ setProjectRecord(sidecar, cwd, { port: record.port, editing: true });
836
+ return true;
837
+ }
826
838
  /**
827
839
  * Keep this project's stored replay switch ({@link ReplayPolicy.enabled}) in
828
840
  * line with what the person asked for, so the recorder never steers — or runs
@@ -225,6 +225,12 @@ function isHookRoute(route) {
225
225
  function isPlanMode(payload) {
226
226
  return payload.permission_mode === "plan";
227
227
  }
228
+ /** The `bir` tools whose `force` saves (or undoes) what the scenario check refused. */
229
+ const FORCEABLE_BIR_TOOLS = new Set(["scenario_edit", "scenario_undo"].map((tool) => `mcp__${SCENARIO_SERVER_KEY}__${tool}`));
230
+ /** A `bir` call that forces a change past the check (see `onToolPre`). */
231
+ function forcedBirChange(toolName, input) {
232
+ return FORCEABLE_BIR_TOOLS.has(toolName) && input?.force === true;
233
+ }
228
234
  /**
229
235
  * Tool arguments as an older proxy reports them — redacted, long strings
230
236
  * clipped — for joining a report that carries no `argsFingerprint` of its own.
@@ -2266,6 +2272,20 @@ export class ControlServer {
2266
2272
  if (toolUseId)
2267
2273
  (run.housekeepingUses ??= new Set()).add(toolUseId);
2268
2274
  logDetail("tool.pre.skipped", { run: run.runId, tool: toolName, why: "host housekeeping" });
2275
+ // `bir setup` lets Claude Code run every `bir` tool without asking — but a
2276
+ // change saved or taken back with `force`, past the check that refused
2277
+ // it, is the person's decision (editSteps.md D3), so Claude Code asks
2278
+ // whatever the allow rule says. Not where the person chose to be asked
2279
+ // nothing at all.
2280
+ if (forcedBirChange(toolName, modelArgs) && payload.permission_mode !== "bypassPermissions") {
2281
+ return {
2282
+ hookSpecificOutput: {
2283
+ hookEventName: "PreToolUse",
2284
+ permissionDecision: "ask",
2285
+ permissionDecisionReason: "force saves what the scenario check refused — only for a recording that was itself wrong; you decide, not the model",
2286
+ },
2287
+ };
2288
+ }
2269
2289
  return {};
2270
2290
  }
2271
2291
  // What a cancelled direct run left this session to tell (see
@@ -1456,15 +1456,17 @@ are where `bir` changes a scenario by hand, so the rules are strict:
1456
1456
  - Exit codes: `0` checked OK, saved or undone; `1` refused, not found, or a service error; `2`
1457
1457
  usage. `--json` prints the service's body unchanged.
1458
1458
 
1459
- The same commands are tools on the `bir` MCP server, which by default lists only `run_scenario`:
1460
- every tool definition rides along with every request of every session. `scenario_show` (`target`,
1461
- `step`), `scenario_edits` (`target`) and `investigate` (`id`) only read; they are listed in a project
1462
- where `bir scenario editing on` was run, or with `BIR_SCENARIO_TOOLS=read` in the `bir` server's
1463
- environment, and answer a host that calls them unlisted all the same. `scenario_check` (`target`,
1464
- `step`, `inputLogic`, `outputLogic`, `unfreeze`), `scenario_edit` (the same, plus `freeze`, `force`,
1465
- `note`, `revision`) and `scenario_undo` (`target`, `edit` or `step`, `force`, `note`) are offered
1466
- only in a project where `bir scenario editing on` was run, because that server runs in every session
1467
- of every installed project. Each tool runs this package's own `bir … --json`, with logic bodies
1459
+ The same commands are tools on the `bir` MCP server. `scenario_show` (`target`, `step`),
1460
+ `scenario_edits` (`target`) and `investigate` (`id`) only read; `scenario_check` (`target`, `step`,
1461
+ `inputLogic`, `outputLogic`, `unfreeze`), `scenario_edit` (the same, plus `freeze`, `force`, `note`,
1462
+ `revision`) and `scenario_undo` (`target`, `edit` or `step`, `force`, `note`) change a scenario. All
1463
+ six are listed where scenario editing is on — `bir setup` turns it on, and adds `mcp__bir` to
1464
+ `permissions.allow` in `.claude/settings.local.json`, so Claude Code runs every `bir` tool without
1465
+ asking. Elsewhere — `bir scenario editing off`, which a later setup keeps, or a project wired by a
1466
+ bare `bir install` — the server lists `run_scenario` alone: it runs in every session of every
1467
+ installed project, and every listed tool's definition rides along with every request.
1468
+ `BIR_SCENARIO_TOOLS=read` in its environment lists the reading three there, and they answer a host
1469
+ that calls them unlisted all the same. Each tool runs this package's own `bir … --json`, with logic bodies
1468
1470
  passed through temporary files; a refusal comes back as an MCP error result carrying the report. A
1469
1471
  reading tool's answer is capped at 24 000 characters (`BIR_SCENARIO_MAX_OUTPUT_CHARS`) and gives up
1470
1472
  after `BIR_READ_TIMEOUT_MS` (15 s) plus 10 s; a changing tool gets 120 s. A call the host cancels, or
@@ -271,12 +271,14 @@ undoes both, leaving everything else as it is:
271
271
  - registers a first-party MCP server under the key **`bir`** in the local scope — `~/.claude.json`,
272
272
  under Claude Code's key for this project, never the `.mcp.json` a team commits (`--mcp-json` puts
273
273
  it there instead, pre-approved in `.claude/settings.local.json`, the older shape). It exposes
274
- `mcp__bir__run_scenario` — the channel a fully-wrapped scenario's results come back through — and
275
- by default nothing else. Three tools that only read a scenario (`scenario_show`,
276
- `scenario_edits`, `investigate`) are listed after `bir scenario editing on`, or with
277
- `BIR_SCENARIO_TOOLS=read` in the server's environment, and three for changing one
278
- (`scenario_check` tries a change, `scenario_edit` saves it, `scenario_undo` takes it back) only
279
- after `bir scenario editing on`. §9.2 lists them with their arguments; the editing it describes
274
+ `mcp__bir__run_scenario` — the channel a fully-wrapped scenario's results come back through. Three
275
+ tools that only read a scenario (`scenario_show`, `scenario_edits`, `investigate`) and three for
276
+ changing one (`scenario_check` tries a change, `scenario_edit` saves it, `scenario_undo` takes it
277
+ back) come with scenario editing: `bir setup` turns it on, and adds `mcp__bir` to
278
+ `permissions.allow` in `.claude/settings.local.json` so Claude Code runs every `bir` tool without
279
+ asking. A bare `bir install --replay` does neither: its server lists `run_scenario` alone until
280
+ `bir scenario editing on` (or `BIR_SCENARIO_TOOLS=read` in the server's environment, for the
281
+ reading three). §9.2 lists them with their arguments; the editing it describes
280
282
  is built but not yet released. From the next version, a call to any of them except
281
283
  `run_scenario` is never recorded. In a subdirectory or linked worktree of a git repository the
282
284
  server is not added — in the local scope it would apply to the whole repository — and replay
@@ -1026,13 +1028,17 @@ check.
1026
1028
  1. **In its terminal.** Claude runs `bir scenario …` like any other command. You see each command,
1027
1029
  and Claude Code asks you before it runs one, unless you allowed it. This works in every project.
1028
1030
  2. **As tools on the `bir` MCP server** — the server `bir setup` or `bir install --replay` adds
1029
- (§5.2). This is smoother for the model. **By default the server lists only `run_scenario`:
1030
- every tool definition rides along with every request of every session. The tools that read and
1031
- change a scenario come when you turn editing on, per project:**
1031
+ (§5.2). This is smoother for the model. **`bir setup` turns scenario editing on, so the server
1032
+ lists every tool below, and Claude Code runs them without asking (`mcp__bir` in
1033
+ `permissions.allow`, `.claude/settings.local.json`) — except `scenario_edit` or `scenario_undo`
1034
+ with `force`: the recorder's `PreToolUse` answers `ask` for those, so the person decides whether
1035
+ a change goes past the check that refused it.** Every listed tool's definition rides along
1036
+ with every request of every session, so the switch is per project, and an `off` is kept by
1037
+ every later setup. A bare `bir install --replay` leaves it off: `run_scenario` alone.
1032
1038
 
1033
1039
  ```bash
1034
1040
  bir scenario editing on # this project's `bir` server also offers the tools that read and change a scenario
1035
- bir scenario editing off # back to run_scenario alone
1041
+ bir scenario editing off # back to run_scenario alone; a later `bir setup` keeps it off
1036
1042
  bir scenario editing status # what is stored for this project
1037
1043
  ```
1038
1044
 
@@ -1045,12 +1051,12 @@ The tools, as the `bir` server defines them. **Bold** arguments are required.
1045
1051
  | Tool (`mcp__bir__…`) | Offered | Arguments | Runs |
1046
1052
  |---|---|---|---|
1047
1053
  | `run_scenario` | always, unless the runner is off | none | the plan armed for this turn (§6.1) |
1048
- | `scenario_show` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | **`target`**, `step` | `bir scenario show` |
1049
- | `scenario_edits` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | **`target`** | `bir scenario edits` |
1050
- | `investigate` | after `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` | `id` — a `run_`, `scn_` or `sexec_` id; without it, the newest turn here | `bir investigate` |
1051
- | `scenario_check` | after `bir scenario editing on` | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `unfreeze` | `bir scenario check` |
1052
- | `scenario_edit` | after `bir scenario editing on` | **`target`**, and **`step`** with `inputLogic`, `outputLogic`, `freeze`, `unfreeze`, `force` — or **`param`** with **`kind`**; `note`, `revision` | `bir scenario edit` |
1053
- | `scenario_undo` | after `bir scenario editing on` | **`target`**, one of `edit`, `step` or `param`, `force`, `note` | `bir scenario undo` |
1054
+ | `scenario_show` | with scenario editing on (`bir setup` turns it on), or with `BIR_SCENARIO_TOOLS=read` | **`target`**, `step` | `bir scenario show` |
1055
+ | `scenario_edits` | with scenario editing on, or with `BIR_SCENARIO_TOOLS=read` | **`target`** | `bir scenario edits` |
1056
+ | `investigate` | with scenario editing on, or with `BIR_SCENARIO_TOOLS=read` | `id` — a `run_`, `scn_` or `sexec_` id; without it, the newest turn here | `bir investigate` |
1057
+ | `scenario_check` | with scenario editing on | **`target`**, **`step`**, `inputLogic`, `outputLogic`, `unfreeze` | `bir scenario check` |
1058
+ | `scenario_edit` | with scenario editing on | **`target`**, and **`step`** with `inputLogic`, `outputLogic`, `freeze`, `unfreeze`, `force` — or **`param`** with **`kind`**; `note`, `revision` | `bir scenario edit` |
1059
+ | `scenario_undo` | with scenario editing on | **`target`**, one of `edit`, `step` or `param`, `force`, `note` | `bir scenario undo` |
1054
1060
 
1055
1061
  - `target` is a `scn_` id, or the `run_` id of a whole-run scenario. `step` is the `stepIndex`.
1056
1062
  - `inputLogic` and `outputLogic` are the code itself, as text. The server writes them to temporary
@@ -1174,8 +1180,9 @@ Parameter database: target → setting (revision 4). Undo: bir scenario undo scn
1174
1180
  | `payload_too_large` | The whole request is larger than the service accepts (HTTP 413, over 256 kB) | A logic body may be at most 20 000 characters, and a note 500 |
1175
1181
  | `rate_limited` | More checks, edits and undos in a minute than the service allows (`SCENARIO_EDITS_PER_MINUTE`, 30 by default) | Wait a minute |
1176
1182
  | `not found` for a scenario you can see | It is not yours. **Only the owner can change a scenario** — an admin gets `not found` too | Ask the owner |
1177
- | The `scenario_check`, `scenario_edit` and `scenario_undo` tools are missing | `bir scenario editing on` was not run in this project, or the session started before it | Run it here, then restart the session |
1178
- | Only `run_scenario` is listed; `scenario_show`, `scenario_edits` and `investigate` are missing | That is the default: the reading tools come with `bir scenario editing on`, or with `BIR_SCENARIO_TOOLS=read` in the server's environment | Either one, then restart the session. Or run the `bir scenario …` commands in the terminal |
1183
+ | The `scenario_check`, `scenario_edit` and `scenario_undo` tools are missing | Scenario editing is off here: `bir scenario editing off` was run (a later setup keeps it off), the project was wired by `bir install` rather than `bir setup`, or the session started before the switch | `bir scenario editing on` here (or `bir setup`), then restart the session |
1184
+ | Only `run_scenario` is listed; `scenario_show`, `scenario_edits` and `investigate` are missing | The same: the reading tools come with scenario editing, or with `BIR_SCENARIO_TOOLS=read` in the server's environment | Either one, then restart the session. Or run the `bir scenario …` commands in the terminal |
1185
+ | Claude Code asks before each `bir` tool | No `mcp__bir` in `permissions.allow`: the project was wired by `bir install`, or the rule was taken out | `bir setup` here adds it (`bir doctor` says when it is missing) |
1179
1186
  | The `bir` server lists no tools at all | The runner is off here (`bir off`, `bir off --here`, or `BIR_OFF=1` where Claude Code started) | `bir on` (or `bir on --here`), and the server tells the open session its list changed. For `BIR_OFF=1`, start Claude Code without it |
1180
1187
 
1181
1188
  ---
@@ -75,7 +75,9 @@ if it is, it stops and asks you to `cd` to the project and paste again. Then:
75
75
  ==> Wiring the project
76
76
  + chrome-devtools → bir-proxy (local scope, over the .mcp.json entry; upstream: npx)
77
77
  + bir → bir-scenario (calculated replay; local scope)
78
+ + scenario editing on: the bir server offers investigate and the scenario tools too (`bir scenario editing off` takes them away)
78
79
  + hooks → C:\work\repo\.claude\settings.local.json (control server on http://127.0.0.1:53411; the recorder starts itself with each session)
80
+ + mcp__bir → permissions.allow in the same file (Claude Code runs the bir tools without asking)
79
81
  + .claude/settings.local.json is listed in C:\work\repo\.git\info\exclude
80
82
 
81
83
  Wrapped 1 server.
@@ -86,6 +88,7 @@ Done. Recording and replay are on for C:\work\repo
86
88
  account you@example.com (https://api.bi2202.com)
87
89
  wrapped chrome-devtools
88
90
  replay on — direct execution allowed for: all wrapped (narrow it with `bir replay allow chrome-devtools`)
91
+ tools run_scenario, scenario_show, scenario_edits, investigate, scenario_check, scenario_edit, scenario_undo — Claude Code runs them without asking, except a forced change
89
92
  recorder started in the background (pid 1234) — it starts itself with every Claude Code session
90
93
  log C:\Users\you\.baseinstrunner\logs\3f9c1a2b7d4e5f60.log
91
94
 
@@ -104,8 +107,11 @@ is not changed;
104
107
  *replay* says which of them a calculated scenario may call unattended — narrow
105
108
  it with `bir replay allow <names>`, turn it off with `bir replay off`, and read
106
109
  [calculatedReplayGuide.md §5.1](calculatedReplayGuide.md) before leaving it wide
107
- anywhere that matters; *recorder* is the background process, and *log* is where
108
- its audit lines go.
110
+ anywhere that matters; *tools* is what the `bir` MCP server gives Claude here —
111
+ every tool, and Claude Code runs them without asking, except a change forced
112
+ past the scenario check (`bir scenario editing off` leaves `run_scenario` alone,
113
+ and a later setup keeps it that way); *recorder* is
114
+ the background process, and *log* is where its audit lines go.
109
115
 
110
116
  A project with no MCP servers in any scope — none in `.mcp.json`, and none for
111
117
  this project or your user in `~/.claude.json` — shows `wrapped (no MCP servers
@@ -190,12 +196,12 @@ its prompt, its tool calls and its final answer.
190
196
  |---|---|
191
197
  | `~/.baseinstrunner/config.json` | the service address; `BIR_AUTH_URL` in the environment overrides it, and `bir setup` warns when the two differ |
192
198
  | `~/.baseinstrunner/credentials.json` | the cached session, bound to the service that issued it |
193
- | `~/.baseinstrunner/installed.json` | what `bir uninstall` needs to undo each change — the original text of the project files it rewrote, with their credential values left out — and each project's port, token and replay switches, and whether Claude's `bir` tools may change scenarios there (`bir scenario editing on`) |
199
+ | `~/.baseinstrunner/installed.json` | what `bir uninstall` needs to undo each change — the original text of the project files it rewrote, with their credential values left out — and each project's port, token and replay switches, whether Claude's `bir` tools may change scenarios there (`bir setup` turns it on, `bir scenario editing off` turns it off), and the permission rule setup added |
194
200
  | `~/.baseinstrunner/logs/<key>.log` | the recorder's audit log, one file per project |
195
201
  | `~/.baseinstrunner/node/` | a private Node, only if the machine had no usable one; not on PATH |
196
202
  | `~/.baseinstrunner/npm/` | the runner package |
197
203
  | `~/.baseinstrunner/bin/` | the `bir` and `bir-hooks` launchers, first on your user PATH |
198
- | `<project>/.claude/settings.local.json` | Claude Code's hooks for this project. Yours, not the team's: it carries this project's token and an absolute Node path |
204
+ | `<project>/.claude/settings.local.json` | Claude Code's hooks for this project, and `mcp__bir` in `permissions.allow` so the `bir` server's tools run without asking. Yours, not the team's: it carries this project's token and an absolute Node path |
199
205
  | `~/.claude.json` (`$CLAUDE_CONFIG_DIR/.claude.json` when that is set), this project's local scope | a copy of each MCP server it wraps, under the same name, running through `bir-proxy`; plus the `bir` scenario server. The copy takes precedence in this project only; `.mcp.json` and your user-scope servers are not changed |
200
206
  | `<project>/.git/info/exclude` | one line, `.claude/settings.local.json`, so it never shows in `git status` |
201
207
 
@@ -363,12 +369,16 @@ The saved code runs on your machine without asking you, like the code the
363
369
  calculation wrote. The check only tests what it sends for the recording's
364
370
  values. So read the code before it is saved, yours or Claude's.
365
371
 
366
- Claude can do this for you. Ask it in the session; it runs the same commands,
367
- and Claude Code asks you before each one, unless you allowed it. Or run
368
- `bir scenario editing on` in the project folder and restart Claude Code: the
369
- `bir` server then also offers the tools that read and change a scenario (by
370
- default it offers only `run_scenario`). These `bir` calls
371
- are never recorded as part of your task.
372
+ Claude can do this for you. Ask it in the session: `bir setup` gave the `bir`
373
+ server the tools that read and change a scenario, and Claude Code runs them
374
+ without asking you. The service still refuses a change that breaks the
375
+ recording, and a change Claude wants to force past that check (`force`) is
376
+ still yours to approve: Claude Code asks you first. `bir scenario edits` shows
377
+ what Claude changed; `bir scenario undo` takes it back. `bir scenario editing off` in the project
378
+ folder takes those tools away (restart Claude Code after), and a later
379
+ `bir setup` leaves them off; Claude can still run the same commands in its
380
+ terminal, and Claude Code then asks you before each one. These `bir` calls are
381
+ never recorded as part of your task.
372
382
 
373
383
  ---
374
384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basein/runner",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/production.md CHANGED
@@ -635,7 +635,7 @@ Truncation is always marked with the original size.
635
635
 
636
636
  ### 6.8 The `bir` MCP server (`src/bin/bir-scenario.ts`, `scenario-liveness.ts`, `service-read.ts`)
637
637
 
638
- **Tools listed.** `tools/list` returns only `run_scenario` by default. The read tools come with `bir scenario editing on` or `BIR_SCENARIO_TOOLS=read`, because every listed tool's definition rides along with every request of every session.
638
+ **Tools listed.** `tools/list` returns every tool where scenario editing is on, which `bir setup` turns on (it also adds `mcp__bir` to `permissions.allow` in `.claude/settings.local.json`, so Claude Code runs them without asking — except `scenario_edit` or `scenario_undo` with `force`, which the recorder's `PreToolUse` answers with `ask` unless the session is in `bypassPermissions` mode). Where it is off — `bir scenario editing off`, which a later setup keeps, or a project wired by a bare `bir install` — it returns only `run_scenario`, and the read tools come with `BIR_SCENARIO_TOOLS=read`, because every listed tool's definition rides along with every request of every session.
639
639
 
640
640
  **`run_scenario`.**
641
641
 
@@ -678,7 +678,7 @@ Truncation is always marked with the original size.
678
678
  | The direct plan budget is 100 s (was 120 s), and direct runs are clamped to 270 s. | Keeps a direct run under Claude Code's 2-minute auto-background of an MCP call, and inside `run_scenario`'s client timeout. | `BIR_REPLAY_PLAN_BUDGET_MS=120000` (or `BIR_REPLAY_BUDGET_MS`). |
679
679
  | The background recorder ignores an inherited `ANTHROPIC_API_KEY` for derivation. | That key is Claude Code's, so derivation billed the user's own account. | `BIR_DERIVE_API_KEY=<key>`, or `BIR_USE_ANTHROPIC_API_KEY=1`. A `bir-hooks` run by hand still uses the terminal's key. |
680
680
  | `run_scenario` never returns `isError`. | An error result makes the model retry or report a failure the user cannot act on. The reason is in `scenario.answered`. | `BIR_SCENARIO_DEBUG=1`. |
681
- | The `bir` server lists only `run_scenario`. | Tool definitions ride along with every request. | `BIR_SCENARIO_TOOLS=read`, or `bir scenario editing on`. |
681
+ | A bare `bir install` leaves the `bir` server listing only `run_scenario`, and adds no permission rule. | Tool definitions ride along with every request, and a fleet wired by hand must not have its plans changed by an agent. `bir setup` turns both on. | `bir scenario editing on` (or `BIR_SCENARIO_TOOLS=read` for the read tools), and `mcp__bir` in `permissions.allow`. |
682
682
  | The `run_scenario` client timeout is 290 s (was 180 s). | It stays under Claude Code's 5-minute MCP idle timeout, above the direct-run worst case. | `BIR_SCENARIO_RUN_TIMEOUT_MS=180000`. |
683
683
  | `bir-proxy` exits when its upstream dies. | Claude Code then shows the server as failed and offers a reconnect, as unwrapped. Staying up left tools listed that only answered errors. | `BIR_PROXY_STAY_UP=1`. |
684
684
  | Proxy stderr is quiet: routine lines only with verbose logging. | Claude Code files every stderr line as a "Server stderr" error. | `BIR_VERBOSE=1`. |