auto-permission-system 3.15.0 → 3.17.0

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.
@@ -65,7 +65,7 @@ async function askDetection(
65
65
  if (choice === "session") {
66
66
  detectionGrants.add(key);
67
67
  audit({ layer: 2, tool: kind, subject, reason, decision: "session", note: "advanced-secure", cwd: ctx.cwd });
68
- ctx.ui.notify(`security-guard: allowed for this conversation (not saved) → ${subject}`, "info");
68
+ ctx.ui.notify(`Allowed for this conversation: ${subject}`, "info");
69
69
  return null;
70
70
  }
71
71
  audit({ layer: 2, tool: kind, subject, reason, decision: "no", note: "advanced-secure", cwd: ctx.cwd });
@@ -130,24 +130,23 @@ export default function (pi: ExtensionAPI) {
130
130
  if (!err) continue;
131
131
  audit({ layer: 2, event: "policy-parse-error", file: p, error: err, cwd: ctx.cwd });
132
132
  console.error(`security-guard: ${p} does not parse (${err}); its rules are NOT applied`);
133
- ctx.ui.notify(`⚠️ security-guard: ${p} does not parse, so neither layer applies its rules (built-in defaults instead).\n${err}`, "error");
133
+ const isProject = p === `${ctx.cwd}/.pi/sandbox.json`;
134
+ const msg = isProject
135
+ ? "⚠️ Syntax error in settings file — project configuration not loaded."
136
+ : "⚠️ Syntax error in settings file — global configuration not loaded.";
137
+ ctx.ui.notify(msg, "error");
134
138
  }
135
139
  setPiDeclinedTrust((ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() === false);
136
140
  const changes = untrustedProjectChanges(ctx.cwd);
137
141
  if (changes.length && !isProjectFileDeclined(projectPolicyPath(ctx.cwd), TRUST_STORE)) {
138
142
  audit({ layer: 2, event: "untrusted-project-policy", file: projectPolicyPath(ctx.cwd), changes, cwd: ctx.cwd });
139
- ctx.ui.notify(
140
- `⚠️ This folder has a .pi/sandbox.json that tries to make your security weaker:\n${bullets(changes)}\n` +
141
- "auto-permission-system ignores these changes. Its block rules still apply.\n" +
142
- "Did you write this file? Then type /security trust.",
143
- "warning",
144
- );
143
+ ctx.ui.notify("🛡️ Project-level configuration detected: run /security trust to approve.", "warning");
145
144
  }
146
145
  const policy = loadPolicy(ctx.cwd);
147
146
  if (!policy.enabled) {
148
147
  policyEnabled = false;
149
148
  applyActive();
150
- ctx.ui.notify("security-guard (Layer 2) disabled: enabled=false in sandbox.json", "info");
149
+ ctx.ui.notify("Tool guard is disabled in your sandbox.json settings.", "info");
151
150
  return;
152
151
  }
153
152
  policyEnabled = true;
@@ -316,15 +315,15 @@ export default function (pi: ExtensionAPI) {
316
315
  if (choice === "program-session" && programId) {
317
316
  grantProgramOutputSession(programId);
318
317
  audit({ layer: 2, tool: event.toolName, decision: "session", grant: programId, note: "advanced-secure-output", findings, cwd: ctx.cwd });
319
- ctx.ui.notify(`🧠 Advanced Secure: you allowed all ${programId} output for the rest of this conversation.`, "info");
318
+ ctx.ui.notify(`Output permitted: allowed all ${programId} output for this conversation.`, "info");
320
319
  } else {
321
320
  audit({ layer: 2, tool: event.toolName, decision: "yes", note: "advanced-secure-output", findings, cwd: ctx.cwd });
322
- ctx.ui.notify(`🧠 Advanced Secure: you allowed ${event.toolName} output to be sent.`, "warning");
321
+ ctx.ui.notify(`Output permitted: allowed ${event.toolName} output for this call.`, "info");
323
322
  }
324
323
  return;
325
324
  }
326
325
  audit({ layer: 2, tool: event.toolName, decision: "no", note: "advanced-secure-output", findings, cwd: ctx.cwd });
327
- ctx.ui.notify(`🧠 Advanced Secure: blocked ${event.toolName} output — it may contain sensitive information. Nothing was sent to the model.`, "warning");
326
+ ctx.ui.notify(`Sensitive data protected: withheld ${event.toolName} output from the AI (potential secrets detected).`, "warning");
328
327
  return { content: [{ type: "text", text: withheldNotice(subject) }] };
329
328
  });
330
329
 
@@ -39,7 +39,7 @@ const MODE_DEFS: Array<{
39
39
  },
40
40
  {
41
41
  value: "yolo",
42
- icon: "⚠️",
42
+ icon: "🚨",
43
43
  name: "YOLO",
44
44
  desc: "Turns off all file sandboxing and leak protections",
45
45
  },
@@ -67,7 +67,7 @@ function renderSandboxChip(ctx: ExtensionContext): void {
67
67
  }
68
68
 
69
69
  function modeNotice(mode: PermissionMode): { text: string; level: "info" | "warning" } {
70
- if (mode === "yolo") return { text: "⚠️ YOLO — all security layers disabled. Saved; new conversations start this way too.", level: "warning" };
70
+ if (mode === "yolo") return { text: "🚨 YOLO — all security layers disabled. Saved; new conversations start this way too.", level: "warning" };
71
71
  if (mode === "advanced-secure") return { text: "🧠 Advanced Secure on. File reads, MCP calls, and tool output are checked for secrets.", level: "info" };
72
72
  return { text: "🛡 Default mode. Rule-based policy only.", level: "info" };
73
73
  }
@@ -97,7 +97,7 @@ async function pickMode(pi: ExtensionAPI, ctx: ExtensionContext, yolo: { active:
97
97
  emitYolo(pi.events, true);
98
98
  audit({ layer: 0, event: "permission-mode", mode: "yolo", cwd: ctx.cwd, note: "runtime-sync" });
99
99
  renderSandboxChip(ctx);
100
- ctx.ui.notify("⚠️ YOLO — all security layers disabled for this conversation.", "warning");
100
+ ctx.ui.notify("🚨 YOLO — all security layers disabled for this conversation.", "warning");
101
101
  return;
102
102
  }
103
103
  ctx.ui.notify(`Permission mode is already ${modeLabel(next)}.`, "info");
@@ -169,7 +169,7 @@ export default function (pi: ExtensionAPI) {
169
169
  if (runtimeYolo) {
170
170
  await stopSandbox();
171
171
  ctx.ui.setStatus("sandbox", ctx.ui.theme.fg("error", YOLO_STATUS));
172
- ctx.ui.notify(ctx.ui.theme.fg("error", "⚠️ YOLO mode — all pi security layers disabled."), "info");
172
+ ctx.ui.notify(ctx.ui.theme.fg("error", "🚨 YOLO mode — all pi security layers disabled."), "info");
173
173
  return;
174
174
  }
175
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-permission-system",
3
- "version": "3.15.0",
3
+ "version": "3.17.0",
4
4
  "description": "Claude auto mode for Pi & security for Pi. A zero-trust permission system for Pi coding agent powered by Advanced Secure mode to scan files, content, and tools to prevent data breaches.",
5
5
  "type": "module",
6
6
  "author": "Navin Kumar",
@@ -86,8 +86,10 @@ export function extractLoopbackEndpoint(output: string, command?: string): Loopb
86
86
 
87
87
  /** A shell-name/empty token is not a path; everything else resolves against cwd unless absolute. */
88
88
  function toAbsolutePath(rawToken: string, cwd: string, home: string): string | undefined {
89
- let token = rawToken.trim().replace(/[.,;]+$/, "");
90
- if (!token || /^(bash|sh|zsh)$/.test(token)) return undefined;
89
+ let token = rawToken.trim().replace(/[.,;:]+$/, "");
90
+ if (!token || /^(bash|sh|zsh|EROFS)$/i.test(token)) return undefined;
91
+ token = token.replace(/\/EROFS$/i, "");
92
+ if (!token) return undefined;
91
93
  if (token === "~" || token.startsWith("~/")) token = home + token.slice(1);
92
94
  const absolute = token.startsWith("/") ? token : `${cwd.replace(/\/+$/, "")}/${token}`;
93
95
  return normalizeAbs(absolute);
@@ -30,7 +30,7 @@ export interface SandboxedBashOpts {
30
30
 
31
31
  /** One denial shape for Layer 1, mirroring denyMessage() in Layer 2. */
32
32
  function blockedLine(action: "Read" | "Write", why: string, outcome: "read" | "written" | "run"): string {
33
- return `🛡️ pi-sandbox: ${action} blocked by policy: ${why}. Nothing was ${outcome}. Assistant guideline: Inform the user and ask if they wish to adjust the permission mode (/permission-mode); do not attempt workarounds or inspect plugin internals.`;
33
+ return `[Security Policy Notice] ${action} blocked by policy: ${why}. Nothing was ${outcome}. Inform the user; do not attempt workarounds.`;
34
34
  }
35
35
 
36
36
  /** `Link` body row for a symlinked block path; empty when the spellings agree (ADR-035). */
@@ -93,7 +93,7 @@ async function preflightSensitiveCommands(
93
93
  return undefined;
94
94
  }
95
95
  auditL1({ subject, decision: "no", note: "sensitive-command", cwd });
96
- onData(Buffer.from(`\n❌ pi-sandbox: command blocked — it can print secrets: ${subject}. Nothing was run — ask the user.\n`));
96
+ onData(Buffer.from(`\n[Security Block] pi-sandbox: command blocked — it can print secrets: ${subject}. Nothing was run — ask the user.\n`));
97
97
  return { exitCode: 1 };
98
98
  }
99
99
 
@@ -116,7 +116,7 @@ async function preflightAskReads(
116
116
  if (sessionAskReadGrants.includes(absPath)) continue;
117
117
  if (!ui?.hasUI || !ui.ui?.select) {
118
118
  auditL1({ subject: absPath, decision: "no", note: "ask-read-headless", cwd });
119
- onData(Buffer.from(`\n❌ pi-sandbox: sensitive file read blocked: ${absPath}. Nothing was run — ask the user.\n`));
119
+ onData(Buffer.from(`\n[Security Block] pi-sandbox: sensitive file read blocked: ${absPath}. Nothing was run — ask the user.\n`));
120
120
  return { exitCode: 1 };
121
121
  }
122
122
  const choice = await askMain(ui, "Let this command read your .env file?", [`File ${absPath}`, `Why .env files usually hold passwords and API keys`, `Risk the AI will see any keys in it`, `Note can't be saved as a permanent rule (it holds secrets)`].join("\n"), { session: true, remember: false, icon: "cred" });
@@ -126,7 +126,7 @@ async function preflightAskReads(
126
126
  continue;
127
127
  }
128
128
  auditL1({ subject: absPath, decision: "no", note: "ask-read", cwd });
129
- onData(Buffer.from(`\n❌ pi-sandbox: sensitive file read blocked: ${absPath}. Nothing was run — ask the user.\n`));
129
+ onData(Buffer.from(`\n[Security Block] pi-sandbox: sensitive file read blocked: ${absPath}. Nothing was run — ask the user.\n`));
130
130
  return { exitCode: 1 };
131
131
  }
132
132
  return undefined;
@@ -327,21 +327,10 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
327
327
  }
328
328
  readDenied = hardDenied || outsideDenied;
329
329
  }
330
- blockedHint = `\n🛡️ Security Policy Notice: filesystem access blocked by sandbox policy.\n`;
331
- if (offending) {
332
- let why = "";
333
- if (writeDenied && !hardDenied) why = " (writes outside allowWrite are read-only)";
334
- else if (hardDenied) why = ` (${hardWhy})`;
335
- else if (outsideDenied) why = " (outside the project directory)";
336
- blockedHint += ` Target: ${offending}${why}\n`;
337
- if (spelledOffending && spelledOffending !== offending) blockedHint += ` Link: ${spelledOffending}\n`;
338
- }
339
- blockedHint +=
340
- ` Explanation: The current sandbox policy restricts this operation to protect your system.\n` +
341
- ` Guideline for Assistant:\n` +
342
- ` • Stop and inform the user clearly about what was blocked.\n` +
343
- ` • Ask the user if they wish to adjust the permission mode (/permission-mode) to allow this action.\n` +
344
- ` • Do not inspect security plugin internals or attempt unauthorized bypasses.\n`;
330
+ let reason = "writes outside allowWrite are read-only";
331
+ if (hardDenied) reason = hardWhy;
332
+ else if (outsideDenied) reason = "target is outside the project directory";
333
+ blockedHint = `\n[Security Policy Notice] filesystem access blocked by sandbox policy: ${writeDenied ? "Write" : "Read"} access to ${offending ?? "target"} is restricted.\nReason: ${reason}. Inform the user; do not attempt workarounds.\n`;
345
334
  } else if (code !== 0 && isLoopbackConnectionRefused(outputTail)) {
346
335
  const endpointInfo = extractLoopbackEndpoint(outputTail, command);
347
336
  const endpointStr = endpointInfo?.port ? `${endpointInfo.host}:${endpointInfo.port}` : (endpointInfo?.host ?? "127.0.0.1");
@@ -356,17 +345,11 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
356
345
  });
357
346
 
358
347
  const notifyMsg = serviceName
359
- ? `pi-sandbox: connection to ${serviceName} on ${endpointStr} was restricted by the sandbox.`
360
- : `pi-sandbox: connection to host ${endpointStr} was restricted by the sandbox.`;
348
+ ? `Local network notice: connection to ${serviceName} (${endpointStr}) was blocked by security policy.`
349
+ : `Local network notice: connection to host ${endpointStr} was blocked by security policy.`;
361
350
  opts?.ctx?.ui?.notify?.(notifyMsg, "warning");
362
351
 
363
- blockedHint = `\n🛡️ Security Policy Notice: Local network connection restricted.\n` +
364
- ` Target: ${endpointStr}${serviceName ? ` (${serviceName})` : ""}\n` +
365
- ` Explanation: The sandbox policy restricted access to this local host address.\n` +
366
- ` Guideline for Assistant:\n` +
367
- ` • Stop and inform the user clearly about what connection was restricted.\n` +
368
- ` • Ask the user if they wish to adjust the permission mode (/permission-mode) to allow this action.\n` +
369
- ` • Do not inspect security plugin internals or attempt unauthorized bypasses.\n`;
352
+ blockedHint = `\n[Security Policy Notice] Connection to ${endpointStr}${serviceName ? ` (${serviceName})` : ""} blocked by sandbox policy.\nReason: Local network connections are restricted. Inform the user; do not attempt workarounds.\n`;
370
353
  }
371
354
  /**
372
355
  * Emit the block explanation only when the command is not re-run with a
@@ -386,8 +369,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
386
369
  if (sandboxSetupError && !hardDenied && !outsideDenied && (!offending || offending === cwd || offending.startsWith(`${cwd}/`))) {
387
370
  blockedHint = "";
388
371
  auditL1({ subject: offending ?? spelledOffending, decision: "write-denied", reason: "the OS sandbox could not start (bwrap mount setup)", cwd: opts?.ctx?.cwd, note: "sandbox-setup-error" });
389
- opts?.ctx?.ui?.notify?.(`pi-sandbox: the OS sandbox could not start (bwrap could not create ${offending ?? spelledOffending ?? "a protected mount point"}). This is a sandbox setup problem, not a policy denial.`, "warning");
390
- decisionHint = `\n❌ pi-sandbox: the OS sandbox could not start (bwrap could not create a protected mount point), so nothing was permission-prompted. This is a sandbox setup problem, not a policy denial.\n`;
372
+ opts?.ctx?.ui?.notify?.(`Sandbox setup notice: could not prepare folder isolation for ${offending ?? spelledOffending ?? "the sandbox"}. System defaults will be used.`, "warning");
373
+ decisionHint = `\n[System Error] pi-sandbox: the OS sandbox could not start (bwrap could not create a protected mount point), so nothing was permission-prompted. This is a sandbox setup problem, not a policy denial.\n`;
391
374
  onData(Buffer.from(decisionHint));
392
375
  } else if (offending && outsideDenied && !hardDenied && outsideMode === "ask" && opts?.ctx?.hasUI && opts.ctx.ui?.select && opts.onAlwaysRead) {
393
376
  const absPath = offending;
@@ -407,8 +390,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
407
390
  if (choice === MainChoice.Once) readOnce.push(absPath);
408
391
  else sessionReadGrants.push(absPath);
409
392
  auditL1({ subject: absPath, decision: choice, scope: choice === MainChoice.Once ? "invocation" : "session", cwd: opts.ctx.cwd, note: "outside-project-read" });
410
- opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"} — re-running the command`, "warning");
411
- onData(Buffer.from(`\n✅ pi-sandbox: read of ${absPath} allowed ${choice === MainChoice.Once ? "once" : "for this conversation"}. Command output with the new permission follows.\n`));
393
+ opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"}`, "info");
394
+ onData(Buffer.from(`\n[Permission Granted] pi-sandbox: read of ${absPath} allowed ${choice === MainChoice.Once ? "once" : "for this conversation"}. Command output with the new permission follows.\n`));
412
395
  resolve(attempt([]));
413
396
  return;
414
397
  }
@@ -417,13 +400,13 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
417
400
  try {
418
401
  const persistedTo = await opts.onAlwaysRead(subject, grant.scope);
419
402
  auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, decision: grant.scope === "cwd" ? "always-cwd" : "always-global", scope: grant.scope, cwd: opts.ctx.cwd, persisted_to: persistedTo, note: "outside-project-read" });
420
- opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${subject} (${grant.scope}${grant.folder ? ", folder" : ""}) — re-running the command`, "warning");
421
- onData(Buffer.from(`\n✅ pi-sandbox: read of ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
403
+ opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
404
+ onData(Buffer.from(`\n[Permission Granted] pi-sandbox: read of ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
422
405
  resolve(attempt([]));
423
406
  return;
424
407
  } catch (e) {
425
408
  auditL1({ subject, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e), note: "outside-project-read" });
426
- decisionHint = `\n❌ pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
409
+ decisionHint = `\n[Security Notice] pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
427
410
  }
428
411
  }
429
412
  } catch (e) {
@@ -445,12 +428,13 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
445
428
  const action: "Read" | "Write" = writeDenied ? "Write" : "Read";
446
429
  const outcome = action === "Write" ? "written" : "read";
447
430
  auditL1({ subject: offending, decision: readDenied && !writeDenied ? "read-denied" : "write-denied", reason: why, cwd });
448
- opts?.ctx?.ui?.notify?.(`pi-sandbox: bash was refused a ${action.toLowerCase()} of ${offending} (${why}). Edit sandbox.json if that is wrong.`, "warning");
431
+ opts?.ctx?.ui?.notify?.(`Folder protected: unable to ${action.toLowerCase()} ${offending}. Location is restricted under current policy.`, "warning");
449
432
  decisionHint = `\n${blockedLine(action, why, outcome)}\n`;
450
433
  onData(Buffer.from(decisionHint));
451
434
  } else if (offending && writeGrantRepeated && writeGrantParent) {
452
435
  auditL1({ subject: offending, decision: "write-denied", reason: `grant for ${writeGrantParent} did not take effect`, cwd: opts?.ctx?.cwd, note: "grant-ineffective" });
453
- opts?.ctx?.ui?.notify?.(`pi-sandbox: the write grant for ${writeGrantParent} did not take effect — the sandbox still refuses ${offending}.`, "warning");
436
+ const targetFolder = (writeGrantParent || offending || "").replace(/\/EROFS$/i, "");
437
+ opts?.ctx?.ui?.notify?.(`🚧 Folder protected: unable to write to ${targetFolder} under sandbox policy. Review sandbox.json or /permission-mode.`, "warning");
454
438
  decisionHint = `\n${blockedLine("Write", `the grant for ${writeGrantParent} did not take effect`, "written")}\n`;
455
439
  onData(Buffer.from(decisionHint));
456
440
  } else if (offending && opts?.ctx?.hasUI && opts.ctx.ui?.select && opts.onAlways) {
@@ -472,8 +456,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
472
456
  if (choice === MainChoice.Once) {
473
457
  grantedWriteFolders.add(parentDir);
474
458
  auditL1({ subject: absPath, granularity: "folder", original: absPath, decision: "once", scope: "invocation", cwd: opts.ctx.cwd, note: "write-once" });
475
- opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${absPath} once — re-running the command`, "warning");
476
- onData(Buffer.from(`\n✅ pi-sandbox: write to ${absPath} allowed once. Command output with the new permission follows.\n`));
459
+ opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${absPath} once`, "info");
460
+ onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${absPath} allowed once. Command output with the new permission follows.\n`));
477
461
  resolve(attempt([...writeOnce, parentDir]));
478
462
  return;
479
463
  }
@@ -482,8 +466,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
482
466
  sessionWriteGrants.push(value);
483
467
  (value === parentDir ? grantedWriteFolders : grantedWriteFiles).add(value);
484
468
  auditL1({ subject: value, granularity: value === parentDir ? "folder" : "file", original: absPath, decision: "session", scope: "session", cwd: opts.ctx.cwd, note: "write-session" });
485
- opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${value} for this conversation (not saved) — re-running the command`, "info");
486
- onData(Buffer.from(`\n✅ pi-sandbox: write to ${value} allowed for this conversation. Command output with the new permission follows.\n`));
469
+ opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${value} for this conversation`, "info");
470
+ onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${value} allowed for this conversation. Command output with the new permission follows.\n`));
487
471
  resolve(attempt([]));
488
472
  return;
489
473
  }
@@ -493,14 +477,14 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
493
477
  const persistedTo = await opts.onAlways(subject, grant.scope);
494
478
  (grant.folder ? grantedWriteFolders : grantedWriteFiles).add(subject);
495
479
  auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, decision: grant.scope === "cwd" ? "always-cwd" : "always-global", scope: grant.scope, cwd: opts.ctx.cwd, persisted_to: persistedTo });
496
- opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${subject} (${grant.scope}${grant.folder ? ", folder" : ""}) — re-running the command`, "warning");
497
- onData(Buffer.from(`\n✅ pi-sandbox: write to ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
480
+ opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
481
+ onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
498
482
  resolve(attempt([]));
499
483
  return;
500
484
  } catch (e) {
501
485
  auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e) });
502
486
  opts.ctx.ui?.notify?.(`pi-sandbox: could not save the permission (${e})`, "error");
503
- decisionHint = `\n❌ pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
487
+ decisionHint = `\n[Security Notice] pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
504
488
  }
505
489
  }
506
490
  } catch (e) {
@@ -519,7 +503,7 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
519
503
  onData(Buffer.from(decisionHint));
520
504
  } else if (offending && writeDenied) {
521
505
  auditL1({ subject: offending, decision: "write-denied", reason: "not under any allowWrite root", cwd });
522
- opts?.ctx?.ui?.notify?.(`pi-sandbox: bash was refused a write of ${offending} (not under any allowWrite root).`, "warning");
506
+ opts?.ctx?.ui?.notify?.(`Folder protected: unable to write to ${offending}. Location is outside allowed project folders.`, "warning");
523
507
  onData(Buffer.from(`\n${blockedLine("Write", "not under any allowWrite root", "written")}\n`));
524
508
  }
525
509
 
@@ -161,7 +161,7 @@ async function decide(deps: NetworkAskDeps, host: string, port: number | undefin
161
161
  if (mainChoice === MainChoice.Session) {
162
162
  sessionGrants.push(wildcard);
163
163
  deps.audit({ ...base, decision: "session", grant: wildcard, requested: host });
164
- ctx.ui.notify?.(`pi-sandbox: allowed for this conversation (not saved) → ${wildcard}`, "info");
164
+ ctx.ui.notify?.(`Allowed for this conversation: ${wildcard}`, "info");
165
165
  return true;
166
166
  }
167
167
 
@@ -176,11 +176,12 @@ async function decide(deps: NetworkAskDeps, host: string, port: number | undefin
176
176
  const path = await deps.persist(pattern, scope);
177
177
  deps.applyLive(pattern);
178
178
  deps.audit({ ...base, decision: scope === GrantScope.Cwd ? "always-cwd" : "always-global", scope, persisted_to: path, pattern, requested: host });
179
- ctx.ui.notify?.(`pi-sandbox: persisted ${scope} override → ${path}`, "warning");
179
+ const scopeLabel = scope === GrantScope.Cwd ? "this project (stored in project settings)" : "all projects (stored in global settings)";
180
+ ctx.ui.notify?.(`Rule saved for ${scopeLabel}.`, "info");
180
181
  } catch (error) {
181
182
  commandGrants?.add(host);
182
183
  deps.audit({ ...base, decision: "yes", note: `always-persist-failed: ${error}`, scope });
183
- ctx.ui.notify?.(`pi-sandbox: could not persist ${scope} override (${error}); allowing this command only`, "warning");
184
+ ctx.ui.notify?.(`Could not save rule for ${scope === GrantScope.Cwd ? "this project" : "all projects"} (${error}); allowing this command only.`, "warning");
184
185
  }
185
186
  return true;
186
187
  }
@@ -87,7 +87,7 @@ export async function askOrBlock(
87
87
  const named = userNamedOutside(ctx as SessionCtx, ask);
88
88
  if (named) {
89
89
  audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision: "user-named", named, cwd: ctx.cwd });
90
- ctx.ui.notify(`security-guard: read outside the project allowed once → ${ask.subject} (you named "${named}")`, "info");
90
+ ctx.ui.notify(`Allowed reading ${ask.subject} once (mentioned in your prompt).`, "info");
91
91
  return null;
92
92
  }
93
93
  }
@@ -102,7 +102,7 @@ export async function askOrBlock(
102
102
  const value = ask.overrideKind === OverrideKind.AllowDomains || isSafeFolderGrant(parent, homedir()) ? parent : ask.overrideValue;
103
103
  sessionGrants.push({ kind: ask.overrideKind, value });
104
104
  audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision, grant: value, cwd: ctx.cwd });
105
- ctx.ui.notify(`security-guard: allowed for this conversation (not saved) → ${value}`, "info");
105
+ ctx.ui.notify(`Allowed for this conversation: ${value}`, "info");
106
106
  return null;
107
107
  }
108
108
  if (decision === Decision.AlwaysCwd || decision === Decision.AlwaysGlobal || decision === Decision.AlwaysCwdFolder || decision === Decision.AlwaysGlobalFolder) {
@@ -117,10 +117,11 @@ export async function askOrBlock(
117
117
  const path = persistOverride(scope, ctx.cwd, ask.overrideKind, value);
118
118
  onPersist?.(ask.overrideKind, value, scope);
119
119
  audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision, scope, granularity: useFolder ? "folder" : "file", cwd: ctx.cwd, persisted_to: path, override: { [ask.overrideKind]: value } });
120
- ctx.ui.notify(`security-guard: persisted ${scope}${useFolder ? " (folder)" : ""} override → ${path}`, "warning");
120
+ const scopeLabel = scope === GrantScope.Cwd ? "this project (stored in project settings)" : "all projects (stored in global settings)";
121
+ ctx.ui.notify(`Rule saved for ${scopeLabel}.`, "info");
121
122
  } catch (error) {
122
123
  audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision: "yes", note: `always-persist-failed: ${error}`, scope, cwd: ctx.cwd });
123
- ctx.ui.notify(`security-guard: could not persist ${scope} override (${error}); allowing this call only`, "warning");
124
+ ctx.ui.notify(`Could not save rule for ${scope === GrantScope.Cwd ? "this project" : "all projects"} (${error}); allowing this call only.`, "warning");
124
125
  }
125
126
  return null;
126
127
  }
@@ -21,7 +21,7 @@ export function subagentNetworkBlock(ctx: { hasUI?: boolean; sessionManager?: un
21
21
  if (ctx.hasUI !== false) return null;
22
22
  const mode = policy.subagent?.network ?? SubagentNetwork.Allow;
23
23
  if (mode === SubagentNetwork.Allow) return null;
24
- if (mode === SubagentNetwork.Deny) return "Network blocked by policy: subagent.network=deny. Nothing was fetched — ask the user.";
24
+ if (mode === SubagentNetwork.Deny) return "[Security Policy Notice] Network access blocked: automated subagent network connections are restricted. Nothing was fetched. Inform the user; do not attempt workarounds.";
25
25
  if (mode === SubagentNetwork.ResearchOnly) {
26
26
  const sessionManager = ctx.sessionManager as { getBranch?: () => Array<{ type: string; text?: string }> } | undefined;
27
27
  const branch = sessionManager?.getBranch?.() ?? [];
@@ -31,7 +31,7 @@ export function subagentNetworkBlock(ctx: { hasUI?: boolean; sessionManager?: un
31
31
  .join(" ")
32
32
  .toLowerCase();
33
33
  for (const agent of RESEARCH_AGENTS) if (recentText.includes(agent)) return null;
34
- return "Network blocked by policy: subagent.network=research-only. Nothing was fetched — ask the user.";
34
+ return "[Security Policy Notice] Network access restricted: subagents may only connect to approved research domains. Nothing was fetched. Inform the user; do not attempt workarounds.";
35
35
  }
36
36
  return null;
37
37
  }
@@ -26,7 +26,7 @@
26
26
  export const YOLO_CHANNEL = "auto-permission-system:yolo";
27
27
 
28
28
  /** Footer text shown while every layer is disabled. */
29
- export const YOLO_STATUS = "⚠️ YOLO — all security layers disabled";
29
+ export const YOLO_STATUS = "🚨 YOLO — all security layers disabled";
30
30
 
31
31
  /** The pi APIs this module needs; kept structural so tests can fake them. */
32
32
  export interface YoloFlagPI {
@@ -482,7 +482,7 @@ export async function askExposure(ctx: AskCtx, hits: string[], programId?: strin
482
482
  */
483
483
  export function withheldNotice(subject: string): string {
484
484
  return [
485
- `\u26a0 Output from \`${subject}\` was withheld because it may contain sensitive information.`,
485
+ `[Security Policy Notice] Output from \`${subject}\` was withheld because it contains sensitive credentials.`,
486
486
  "",
487
487
  "The tool completed successfully, but the output is unavailable.",
488
488
  "Continue with the available information, or ask the user to allow access if the output is needed.",