Skip to content

Commit bf3925f

Browse files
committed
Surface persisted skip-permissions default at startup
1 parent 3f652ce commit bf3925f

6 files changed

Lines changed: 77 additions & 2 deletions

File tree

‎docs/IMPLEMENTATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Intent defaults: `intent=implement` → director `build`; explore/plan → same-
166166

167167
### Auto Mode
168168

169-
Auto mode defaults **on** (`config.auto = true` from `loadConfig`; pass `--no-auto` to start off, or `--auto` to force on). It is toggled only via those CLI flags — there is currently no in-session key bound to it. The permission gate reads the flag (`getAuto`/`setAuto` in `src/permission/gate.ts`) on the next tool call. `--dangerously-skip-permissions` still forces this process. `/yolo [on|off|toggle]` (bare `/yolo` also toggles) persists as the user-global default and wires `getSkipPermissions`/`setSkipPermissions` so the gate and pre-gate sandboxes honor the change on the next tool call without rebuilding plugins. `/yolo` writes the same `config.globalSettingsPath` target as the other `/settings`-style toggles, including a `--config` override. Secret-guard and authz still apply.
169+
Auto mode defaults **on** (`config.auto = true` from `loadConfig`; pass `--no-auto` to start off, or `--auto` to force on). It is toggled only via those CLI flags — there is currently no in-session key bound to it. The permission gate reads the flag (`getAuto`/`setAuto` in `src/permission/gate.ts`) on the next tool call. `--dangerously-skip-permissions` still forces this process. `/yolo [on|off|toggle]` (bare `/yolo` also toggles) persists as the user-global default and wires `getSkipPermissions`/`setSkipPermissions` so the gate and pre-gate sandboxes honor the change on the next tool call without rebuilding plugins. `/yolo` writes the same `config.globalSettingsPath` target as the other `/settings`-style toggles, including a `--config` override. Secret-guard and authz still apply. `loadConfig` tracks `skipPermissionsFromSettings` (true only when the effective value came from persisted settings, not the CLI flag) so `runTUI` can show a startup notice and `exec` can print an equivalent stderr warning for the otherwise-silent persisted default.
170170

171171
When auto is on, the gate auto-allows workspace file tools in `AUTO_ALLOWED_TOOLS` and any `run_shell` that does not match the auto-shell policy. The policy (`autoShellRuleForCall` / `AUTO_SHELL_RULES` in `src/permission/auto-shell-policy.ts`) peels wrappers via `expandShellSubjects` (`bash`/`sh`/`zsh -c`, `xargs`, transparent prefixes), then applies:
172172

‎docs/PRODUCT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ is the direct, explicit resume path.
9797

9898
## Slash Commands (TUI)
9999

100-
The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/yolo` (persists as the user-global skip-permissions default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. Plugins can register additional commands.
100+
The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/yolo` (persists as the user-global skip-permissions default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. When a session starts with the persisted default already on, the TUI shows a startup notice ("Permission prompts are disabled by your saved default…") so the silent machine-wide default is never invisible; `corbits exec` prints the equivalent warning to stderr. Plugins can register additional commands.
101101

102102
**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`. Each one is a Skywalker recipe — the slash sends the skill body to the primary, which then `task(agent="<director>")`. `/scribe` → shakespeare; `/implement` spawns build / greybeard / critique as the recipe specifies; `/plan` → plan director (eng change plan: files, AC, non-goals, risks, ordered steps; does not implement); `/review` is a code-review action. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). Dispatch is not a default slash — it stays `use_skill` only, along with git-rebase, linear-issue-workflow, style, philosophy, typescript, and opsh (`user-invocable: false`). Draper and emil are not slashes; they remain closed directors via `task(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `use_skill`. Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone.
103103

‎src/config.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ describe("loadConfig", () => {
491491
globalSettingsPath: globalPath,
492492
});
493493
expect(config.dangerouslySkipPermissions).toBe(false);
494+
expect(config.skipPermissionsFromSettings).toBe(false);
494495
} finally {
495496
await rm(cwd, { recursive: true, force: true });
496497
}
@@ -505,6 +506,8 @@ describe("loadConfig", () => {
505506
{ globalSettingsPath: globalPath },
506507
);
507508
expect(config.dangerouslySkipPermissions).toBe(true);
509+
// Came from the CLI flag, not the persisted default — no startup notice.
510+
expect(config.skipPermissionsFromSettings).toBe(false);
508511
} finally {
509512
await rm(cwd, { recursive: true, force: true });
510513
}
@@ -532,6 +535,9 @@ describe("loadConfig", () => {
532535
globalSettingsPath: globalPath,
533536
});
534537
expect(config.dangerouslySkipPermissions).toBe(true);
538+
// Origin is the persisted default, not this invocation's flag — the
539+
// startup notice should fire.
540+
expect(config.skipPermissionsFromSettings).toBe(true);
535541
} finally {
536542
await rm(cwd, { recursive: true, force: true });
537543
}
@@ -559,6 +565,7 @@ describe("loadConfig", () => {
559565
globalSettingsPath: globalPath,
560566
});
561567
expect(config.dangerouslySkipPermissions).toBe(false);
568+
expect(config.skipPermissionsFromSettings).toBe(false);
562569
} finally {
563570
await rm(cwd, { recursive: true, force: true });
564571
}
@@ -587,6 +594,8 @@ describe("loadConfig", () => {
587594
{ globalSettingsPath: globalPath },
588595
);
589596
expect(config.dangerouslySkipPermissions).toBe(true);
597+
// CLI flag wins over settings — no notice is warranted here.
598+
expect(config.skipPermissionsFromSettings).toBe(false);
590599
} finally {
591600
await rm(cwd, { recursive: true, force: true });
592601
}
@@ -616,11 +625,46 @@ describe("loadConfig", () => {
616625
assertConfigured(config);
617626
expect(config.command).toBe("exec");
618627
expect(config.dangerouslySkipPermissions).toBe(true);
628+
expect(config.skipPermissionsFromSettings).toBe(true);
619629
} finally {
620630
await rm(cwd, { recursive: true, force: true });
621631
}
622632
});
623633

634+
test("persisted skip-permissions default applies regardless of cwd (machine-wide scope)", async () => {
635+
// The global settings file is machine-wide: a session opened against a
636+
// completely different cwd still inherits the same default. This is the
637+
// exact silent-everywhere behavior the startup notice exists to surface.
638+
const globalCwd = await emptyCwd();
639+
const otherCwd = await emptyCwd();
640+
try {
641+
const globalPath = join(globalCwd, "global.json");
642+
await writeFile(
643+
globalPath,
644+
JSON.stringify({
645+
defaultProvider: "fireworks",
646+
providers: {
647+
fireworks: {
648+
baseURL: "https://api.fireworks.ai/inference",
649+
apiKey: "test-key",
650+
models: ["accounts/fireworks/routers/kimi-k2p6-turbo"],
651+
},
652+
},
653+
dangerouslySkipPermissions: true,
654+
}),
655+
);
656+
const config = await loadConfig(["--cwd", otherCwd, "do something"], {
657+
globalSettingsPath: globalPath,
658+
});
659+
expect(config.cwd).toBe(otherCwd);
660+
expect(config.dangerouslySkipPermissions).toBe(true);
661+
expect(config.skipPermissionsFromSettings).toBe(true);
662+
} finally {
663+
await rm(globalCwd, { recursive: true, force: true });
664+
await rm(otherCwd, { recursive: true, force: true });
665+
}
666+
});
667+
624668
test("reads provider and model from a --config settings file", async () => {
625669
const cwd = await emptyCwd();
626670
try {

‎src/config/index.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ export type Config = {
279279
task: string;
280280
force: boolean;
281281
dangerouslySkipPermissions: boolean;
282+
// True when dangerouslySkipPermissions came from the persisted global
283+
// default rather than this invocation's CLI flag. Entry points use this to
284+
// surface a startup notice since the persisted default is otherwise silent.
285+
skipPermissionsFromSettings: boolean;
282286
auto: boolean;
283287
/**
284288
* Exec-only chosen primary director. Omitted = Skywalker (product default).
@@ -348,6 +352,7 @@ export type UnconfiguredConfig = {
348352
task: string;
349353
force: boolean;
350354
dangerouslySkipPermissions: boolean;
355+
skipPermissionsFromSettings: boolean;
351356
auto: boolean;
352357
command: "tui" | "exec";
353358
/** Exec-only chosen primary. Omitted on the unconfigured path too. */
@@ -388,6 +393,9 @@ Flags:
388393
--force override an existing run state
389394
--director <id> exec-only: run as this director (default: skywalker)
390395
--dangerously-skip-permissions
396+
skip permission prompts for this run only;
397+
/yolo in the TUI instead persists the default
398+
machine-wide in ~/.corbits/settings.json
391399
--auto / --no-auto auto mode on/off
392400
--help, -h show this help
393401
`;
@@ -589,6 +597,11 @@ export async function loadConfig(
589597
})
590598
: await loadSettings(options.globalSettingsPath ?? globalSettingsPath());
591599

600+
// Track whether the effective value came from the persisted global default
601+
// rather than this invocation's --dangerously-skip-permissions flag, so the
602+
// TUI/exec entry points can surface a startup notice for the silent case.
603+
const skipPermissionsFromSettings =
604+
!dangerouslySkipPermissions && settings?.dangerouslySkipPermissions === true;
592605
dangerouslySkipPermissions =
593606
dangerouslySkipPermissions || settings?.dangerouslySkipPermissions === true;
594607

@@ -656,6 +669,7 @@ export async function loadConfig(
656669
task,
657670
force,
658671
dangerouslySkipPermissions,
672+
skipPermissionsFromSettings,
659673
auto,
660674
command,
661675
...(director !== undefined ? { director } : {}),
@@ -708,6 +722,7 @@ export async function loadConfig(
708722
task: resumeTask,
709723
force,
710724
dangerouslySkipPermissions,
725+
skipPermissionsFromSettings,
711726
auto,
712727
command,
713728
...(director !== undefined ? { director } : {}),

‎src/exec/runner.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ export async function runExec(config: Config): Promise<ExecResult> {
358358

359359
const interactive = input.isTTY === true && output.isTTY === true;
360360

361+
if (config.skipPermissionsFromSettings) {
362+
stderr.write(
363+
"Warning: permission prompts are disabled by your saved default (/yolo off to re-enable).\n",
364+
);
365+
}
366+
361367
const permissionGate = createPermissionGate({
362368
approvals: seededApprovals,
363369
telemetry: liveTelemetry,

‎src/tui/runner.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,6 +2585,16 @@ export async function runTUI(initialConfig: Config): Promise<number> {
25852585
paintPluginAttention = (needs) => setPluginNeedsAttention(host.shell, needs);
25862586
paintPluginAttention(standingPluginWarnings.length > 0);
25872587

2588+
// The persisted /yolo default is otherwise silent: nothing on screen would
2589+
// otherwise tell the operator that permission prompts are off for a repo
2590+
// they never ran --dangerously-skip-permissions or /yolo in.
2591+
if (config.skipPermissionsFromSettings) {
2592+
surfaceSystemNotice(
2593+
host.shell,
2594+
"Permission prompts are disabled by your saved default (/yolo off to re-enable).",
2595+
);
2596+
}
2597+
25882598
// Soft upgrade check: never blocks startup; offline / rate-limit is a quiet skip.
25892599
// surfaceSystemNotice keeps the landing hero up and flushes into the transcript
25902600
// once a session row ends the landing (same path as MCP startup chatter).

0 commit comments

Comments
 (0)