Skip to content

Commit 98229c1

Browse files
committed
Surface persisted skip-permissions default at startup
1 parent 418891f commit 98229c1

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: implement/explore/plan → same-named director; review → crit
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 implement / 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
@@ -433,6 +433,7 @@ describe("loadConfig", () => {
433433
globalSettingsPath: globalPath,
434434
});
435435
expect(config.dangerouslySkipPermissions).toBe(false);
436+
expect(config.skipPermissionsFromSettings).toBe(false);
436437
} finally {
437438
await rm(cwd, { recursive: true, force: true });
438439
}
@@ -447,6 +448,8 @@ describe("loadConfig", () => {
447448
{ globalSettingsPath: globalPath },
448449
);
449450
expect(config.dangerouslySkipPermissions).toBe(true);
451+
// Came from the CLI flag, not the persisted default — no startup notice.
452+
expect(config.skipPermissionsFromSettings).toBe(false);
450453
} finally {
451454
await rm(cwd, { recursive: true, force: true });
452455
}
@@ -474,6 +477,9 @@ describe("loadConfig", () => {
474477
globalSettingsPath: globalPath,
475478
});
476479
expect(config.dangerouslySkipPermissions).toBe(true);
480+
// Origin is the persisted default, not this invocation's flag — the
481+
// startup notice should fire.
482+
expect(config.skipPermissionsFromSettings).toBe(true);
477483
} finally {
478484
await rm(cwd, { recursive: true, force: true });
479485
}
@@ -501,6 +507,7 @@ describe("loadConfig", () => {
501507
globalSettingsPath: globalPath,
502508
});
503509
expect(config.dangerouslySkipPermissions).toBe(false);
510+
expect(config.skipPermissionsFromSettings).toBe(false);
504511
} finally {
505512
await rm(cwd, { recursive: true, force: true });
506513
}
@@ -529,6 +536,8 @@ describe("loadConfig", () => {
529536
{ globalSettingsPath: globalPath },
530537
);
531538
expect(config.dangerouslySkipPermissions).toBe(true);
539+
// CLI flag wins over settings — no notice is warranted here.
540+
expect(config.skipPermissionsFromSettings).toBe(false);
532541
} finally {
533542
await rm(cwd, { recursive: true, force: true });
534543
}
@@ -558,11 +567,46 @@ describe("loadConfig", () => {
558567
assertConfigured(config);
559568
expect(config.command).toBe("exec");
560569
expect(config.dangerouslySkipPermissions).toBe(true);
570+
expect(config.skipPermissionsFromSettings).toBe(true);
561571
} finally {
562572
await rm(cwd, { recursive: true, force: true });
563573
}
564574
});
565575

576+
test("persisted skip-permissions default applies regardless of cwd (machine-wide scope)", async () => {
577+
// The global settings file is machine-wide: a session opened against a
578+
// completely different cwd still inherits the same default. This is the
579+
// exact silent-everywhere behavior the startup notice exists to surface.
580+
const globalCwd = await emptyCwd();
581+
const otherCwd = await emptyCwd();
582+
try {
583+
const globalPath = join(globalCwd, "global.json");
584+
await writeFile(
585+
globalPath,
586+
JSON.stringify({
587+
defaultProvider: "fireworks",
588+
providers: {
589+
fireworks: {
590+
baseURL: "https://api.fireworks.ai/inference",
591+
apiKey: "test-key",
592+
models: ["accounts/fireworks/routers/kimi-k2p6-turbo"],
593+
},
594+
},
595+
dangerouslySkipPermissions: true,
596+
}),
597+
);
598+
const config = await loadConfig(["--cwd", otherCwd, "do something"], {
599+
globalSettingsPath: globalPath,
600+
});
601+
expect(config.cwd).toBe(otherCwd);
602+
expect(config.dangerouslySkipPermissions).toBe(true);
603+
expect(config.skipPermissionsFromSettings).toBe(true);
604+
} finally {
605+
await rm(globalCwd, { recursive: true, force: true });
606+
await rm(otherCwd, { recursive: true, force: true });
607+
}
608+
});
609+
566610
test("reads provider and model from a --config settings file", async () => {
567611
const cwd = await emptyCwd();
568612
try {

src/config/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ export type Config = {
277277
task: string;
278278
force: boolean;
279279
dangerouslySkipPermissions: boolean;
280+
// True when dangerouslySkipPermissions came from the persisted global
281+
// default rather than this invocation's CLI flag. Entry points use this to
282+
// surface a startup notice since the persisted default is otherwise silent.
283+
skipPermissionsFromSettings: boolean;
280284
auto: boolean;
281285
/**
282286
* Entry mode. `"tui"` is the interactive Ink shell; `"exec"` is the non-TUI
@@ -341,6 +345,7 @@ export type UnconfiguredConfig = {
341345
task: string;
342346
force: boolean;
343347
dangerouslySkipPermissions: boolean;
348+
skipPermissionsFromSettings: boolean;
344349
auto: boolean;
345350
command: "tui" | "exec";
346351
// Path where the onboarding flow should write the new settings.
@@ -378,6 +383,9 @@ Flags:
378383
--resume interactive session picker
379384
--force override an existing run state
380385
--dangerously-skip-permissions
386+
skip permission prompts for this run only;
387+
/yolo in the TUI instead persists the default
388+
machine-wide in ~/.corbits/settings.json
381389
--auto / --no-auto auto mode on/off
382390
--help, -h show this help
383391
`;
@@ -567,6 +575,11 @@ export async function loadConfig(
567575
})
568576
: await loadSettings(options.globalSettingsPath ?? globalSettingsPath());
569577

578+
// Track whether the effective value came from the persisted global default
579+
// rather than this invocation's --dangerously-skip-permissions flag, so the
580+
// TUI/exec entry points can surface a startup notice for the silent case.
581+
const skipPermissionsFromSettings =
582+
!dangerouslySkipPermissions && settings?.dangerouslySkipPermissions === true;
570583
dangerouslySkipPermissions =
571584
dangerouslySkipPermissions || settings?.dangerouslySkipPermissions === true;
572585

@@ -634,6 +647,7 @@ export async function loadConfig(
634647
task,
635648
force,
636649
dangerouslySkipPermissions,
650+
skipPermissionsFromSettings,
637651
auto,
638652
command,
639653
globalSettingsPath: effectiveSettingsPath,
@@ -685,6 +699,7 @@ export async function loadConfig(
685699
task: resumeTask,
686700
force,
687701
dangerouslySkipPermissions,
702+
skipPermissionsFromSettings,
688703
auto,
689704
command,
690705
globalSettingsPath: effectiveSettingsPath,

src/exec/runner.ts

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

322322
const interactive = input.isTTY === true && output.isTTY === true;
323323

324+
if (config.skipPermissionsFromSettings) {
325+
stderr.write(
326+
"Warning: permission prompts are disabled by your saved default (/yolo off to re-enable).\n",
327+
);
328+
}
329+
324330
const permissionGate = createPermissionGate({
325331
approvals: seededApprovals,
326332
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)