@yagni-app/code 1.0.7 → 1.0.8

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.
@@ -12,18 +12,20 @@
12
12
  * for a session restart, the same way Claude persists the toggle.
13
13
  */
14
14
  import { spawn } from "node:child_process";
15
- import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from "node:fs";
16
- import { dirname, join } from "node:path";
15
+ import { existsSync } from "node:fs";
16
+ import { join } from "node:path";
17
17
  import { Type } from "typebox";
18
18
  import { Key } from "@earendil-works/pi-tui";
19
19
  import { createBashToolDefinition, getShellConfig, SettingsManager, } from "@earendil-works/pi-coding-agent";
20
20
  import { logEvent } from "../errorSink.js";
21
21
  import { codeStateHome } from "../stateHome.js";
22
22
  import { isDebug } from "../diagnostics.js";
23
+ import { mutateConfigJson, mutateLocalConfig } from "../settingsFiles.js";
23
24
  import { loadSandboxSettings } from "./config.js";
24
25
  import { annotateCommandOutput, makeSandboxSpawnHook, preWrappedCommand, shouldUseSandbox, shouldUseSandboxForUserCommand, } from "./bash.js";
25
26
  import { YagniSandboxManager } from "./manager.js";
26
27
  import { SandboxPanel, buildPanelState } from "./panel.js";
28
+ import { effectiveRules } from "../permissionRules/loadConfig.js";
27
29
  /**
28
30
  * Build the sandbox bash composition: given ANY stock bash definition (pi's
29
31
  * own or condensedTools'), return the sandbox-aware version — schema gains
@@ -177,6 +179,33 @@ export function registerSandbox(pi, opts) {
177
179
  if (ctx?.ui)
178
180
  uiBridge = ctx;
179
181
  };
182
+ // Trust snapshot for the sandbox rule merge (D3): an untrusted repo's
183
+ // committed allow rules (project OR local) must not widen the OS-level
184
+ // allowWrite/deniedDomains sets — the same filtering the permission gate
185
+ // applies. Captured from the latest ctx; defaults trusted (parity with
186
+ // the gate's fail-open posture on a missing isProjectTrusted).
187
+ let trustedSnapshot = true;
188
+ const captureTrust = (ctx) => {
189
+ try {
190
+ if (typeof ctx?.isProjectTrusted === "function")
191
+ trustedSnapshot = ctx.isProjectTrusted();
192
+ }
193
+ catch (err) {
194
+ // Keep the prior snapshot (fail-open parity with the gate) but make
195
+ // the throw VISIBLE: warn (not debug) — readSessionTrail filters debug
196
+ // lines, so a debug event would never reach /feedback; a trust-probe
197
+ // failure silently pinning the wrong posture must be surfacable.
198
+ // Scrub-safe: error class only, no thrown message.
199
+ logEvent({
200
+ source: "sandbox",
201
+ level: "warn",
202
+ event: "trust_probe_failed",
203
+ fields: { error: err instanceof Error ? err.constructor.name : typeof err },
204
+ });
205
+ }
206
+ };
207
+ /** Rules filtered to what the sandbox may enforce OS-wide (trust-aware). */
208
+ const effectiveRulesForSandbox = () => effectiveRules(rules, trustedSnapshot);
180
209
  const sessionDomainGrants = new Set();
181
210
  const askHandler = async (host) => {
182
211
  if (sessionDomainGrants.has(host))
@@ -212,9 +241,11 @@ export function registerSandbox(pi, opts) {
212
241
  try {
213
242
  persistDomainGrant(opts.projectRoot ?? opts.cwd, host, resolvedStateHome);
214
243
  }
215
- catch {
244
+ catch (err) {
216
245
  // fail-soft: the session grant already holds; the persistence just
217
- // did not — the user chose it, so surface it rather than stay silent.
246
+ // did not — the user chose it, so surface it (notify + sink, same
247
+ // posture as persistToggleAndNotify) rather than stay silent.
248
+ logGrantPersistFailure(host, "project", err);
218
249
  ui.ui.notify?.("Could not persist the domain grant to the config file — it applies to this session only.", "warning");
219
250
  }
220
251
  }
@@ -222,12 +253,13 @@ export function registerSandbox(pi, opts) {
222
253
  try {
223
254
  persistDomainGrant(null, host, resolvedStateHome);
224
255
  }
225
- catch {
256
+ catch (err) {
257
+ logGrantPersistFailure(host, "user", err);
226
258
  ui.ui.notify?.("Could not persist the domain grant to the config file — it applies to this session only.", "warning");
227
259
  }
228
260
  }
229
261
  // The grant must reach the live srt config before the retry.
230
- manager.refreshConfig([...rules]);
262
+ manager.refreshConfig([...effectiveRulesForSandbox()]);
231
263
  return true;
232
264
  };
233
265
  // --- bash composition ---
@@ -254,7 +286,7 @@ export function registerSandbox(pi, opts) {
254
286
  },
255
287
  grantDomainSession: (domain) => {
256
288
  sessionDomainGrants.add(domain);
257
- manager.refreshConfig([...rules]);
289
+ manager.refreshConfig([...effectiveRulesForSandbox()]);
258
290
  },
259
291
  sessionDomains: () => [...sessionDomainGrants],
260
292
  composeBash,
@@ -322,10 +354,12 @@ export function registerSandbox(pi, opts) {
322
354
  // (fired from inside bash execution, no event ctx) can prompt.
323
355
  pi.on("tool_call", async (_event, ctx) => {
324
356
  captureUi(ctx);
357
+ captureTrust(ctx);
325
358
  return;
326
359
  });
327
360
  pi.on("session_start", async (_event, ctx) => {
328
361
  captureUi(ctx);
362
+ captureTrust(ctx);
329
363
  currentSettings = load();
330
364
  // --no-sandbox skips init in EVERY mode — headless included. The notify
331
365
  // is UI-gated, the SKIP never is (a UI-only skip would silently keep the
@@ -338,7 +372,7 @@ export function registerSandbox(pi, opts) {
338
372
  if (!currentSettings.enabled)
339
373
  return;
340
374
  manager.setAskHandler(askHandler);
341
- const err = await manager.initialize(rules);
375
+ const err = await manager.initialize(effectiveRulesForSandbox());
342
376
  if (err) {
343
377
  logEvent({
344
378
  source: "sandbox",
@@ -391,21 +425,57 @@ export function registerSandbox(pi, opts) {
391
425
  };
392
426
  /**
393
427
  * Session-level toggle. OFF is always immediate (manager.reset unwraps the
394
- * next command). ON is split by how the session started:
428
+ * next command) and when the session started ENABLED — PERSISTS the
429
+ * disable to the local config (Claude's toggle persists both directions).
430
+ * ON is split by how the session started:
395
431
  * - config-enabled session: re-initialize (force past nothing — it was on)
396
- * - default-off session: the wrapped bash may not have registered at load
432
+ * - default-off session: the wrapped bash may not have registered at load
397
433
  * (condensedTools owns the registration in the common TUI and reads disk
398
434
  * settings, not this session's toggle). So /sandbox enable PERSISTS
399
- * sandbox.enabled=true to the user config (Claude's own panel persists
435
+ * sandbox.enabled=true to the local config (Claude's own panel persists
400
436
  * its mode choice the same way) and asks for a restart — the honest
401
437
  * behavior; a silent no-op would be a security lie.
402
438
  */
439
+ /** Persist-or-notify for the enable/disable toggles — keeps
440
+ * setSessionToggle flat: try the local write, return the success notice
441
+ * on success or the failure notice on throw (the caller decides level +
442
+ * side effects like the sessionToggle reset). A failed persist also logs
443
+ * sandbox_persist_failed with the thrown message (path/reason text from
444
+ * mutateConfigJson) — same diagnosability posture as rule_save_failed. */
445
+ const persistToggleAndNotify = (enabled, ctx) => {
446
+ try {
447
+ persistEnabledFlip(enabled, opts.cwd);
448
+ return {
449
+ ok: true,
450
+ notice: enabled
451
+ ? "Sandbox enabled in .yagni-code/config.local.json — restart the session (or /new) to apply it to the model's bash tool. Your ! commands are NOT sandboxed until then."
452
+ : "Sandbox disabled for this session and set to off in .yagni-code/config.local.json",
453
+ };
454
+ }
455
+ catch (err) {
456
+ logEvent({
457
+ source: "sandbox",
458
+ level: "warn",
459
+ event: "sandbox_persist_failed",
460
+ fields: {
461
+ enabled,
462
+ error: err instanceof Error ? err.message : String(err),
463
+ },
464
+ });
465
+ return {
466
+ ok: false,
467
+ notice: enabled
468
+ ? 'Could not write the sandbox enable to .yagni-code/config.local.json — set "sandbox": { "enabled": true } there and restart.'
469
+ : "Sandbox disabled for this session (could not persist the off state to config.local.json — a restart may re-arm it)",
470
+ };
471
+ }
472
+ };
403
473
  const setSessionToggle = async (on, ctx) => {
404
474
  sessionToggle = on;
405
475
  if (on) {
406
476
  const startedEnabled = load().enabled;
407
477
  if (startedEnabled) {
408
- const err = await manager.initialize(rules, { force: true });
478
+ const err = await manager.initialize(effectiveRulesForSandbox(), { force: true });
409
479
  if (err) {
410
480
  ctx.ui.notify(`Sandbox unavailable: ${err}`, "error");
411
481
  sessionToggle = null;
@@ -424,13 +494,13 @@ export function registerSandbox(pi, opts) {
424
494
  else {
425
495
  // Persist the enable (fail-soft: on write failure tell the user to
426
496
  // edit the file — never claim success falsely).
427
- try {
428
- persistEnabledFlip(true, resolvedStateHome);
497
+ const r = persistToggleAndNotify(true, ctx);
498
+ if (r.ok) {
429
499
  ctx.ui.setStatus?.("sandbox", "sandbox on (restart to apply)");
430
- ctx.ui.notify("Sandbox enabled in ~/.yagni-code/config.json — restart the session (or /new) to apply it to the model's bash tool. Your ! commands are NOT sandboxed until then.", "warning");
500
+ ctx.ui.notify(r.notice, "warning");
431
501
  }
432
- catch {
433
- ctx.ui.notify("Could not write the sandbox enable to ~/.yagni-code/config.json — set \"sandbox\": { \"enabled\": true } there and restart.", "error");
502
+ else {
503
+ ctx.ui.notify(r.notice, "error");
434
504
  sessionToggle = null;
435
505
  }
436
506
  }
@@ -438,19 +508,32 @@ export function registerSandbox(pi, opts) {
438
508
  else {
439
509
  await manager.reset();
440
510
  ctx.ui.setStatus?.("sandbox", "");
441
- ctx.ui.notify("Sandbox disabled for this session", "warning");
511
+ // Persist the disable when the session started enabled — a restart
512
+ // must not silently re-arm the sandbox the user just turned off.
513
+ // Fail-soft: the session-level reset above already applies NOW.
514
+ if (load().enabled) {
515
+ const r = persistToggleAndNotify(false, ctx);
516
+ // Error (not warning): a failed off-persist silently re-arms the
517
+ // sandbox on restart — the security-relevant direction, at least as
518
+ // loud as the enable failure.
519
+ ctx.ui.notify(r.notice, r.ok ? "warning" : "error");
520
+ }
521
+ else {
522
+ ctx.ui.notify("Sandbox disabled for this session", "warning");
523
+ }
442
524
  }
443
525
  };
444
526
  /**
445
527
  * The interactive /sandbox panel (TUI only). Selections persist to the
446
- * user config (see persistSandboxMode/persistSandboxOverride for the
447
- * interim-destination reasoning); the confirmation message is surfaced
448
- * via notify after the panel closes, mirroring Claude's onComplete flow.
449
- * enable-from-default-off keeps the honest restart advice.
528
+ * PROJECT-LOCAL config (.yagni-code/config.local.json the personal
529
+ * per-project tier, Claude Code's own panel destination); the
530
+ * confirmation message is surfaced via notify after the panel closes,
531
+ * mirroring Claude's onComplete flow. enable-from-default-off keeps the
532
+ * honest restart advice.
450
533
  */
451
534
  const openSandboxPanel = async (ctx) => {
452
535
  const deps = manager.checkDependencies();
453
- const state = buildPanelState(currentSettings, sessionToggle === false, rules, {
536
+ const state = buildPanelState(currentSettings, sessionToggle === false, effectiveRulesForSandbox(), {
454
537
  cwd: opts.cwd,
455
538
  userStateHome: resolvedStateHome,
456
539
  projectRoot: opts.projectRoot ?? null,
@@ -460,11 +543,11 @@ export function registerSandbox(pi, opts) {
460
543
  onModeSelect: async (choice) => {
461
544
  const wasEnabled = load().enabled === true;
462
545
  try {
463
- persistSandboxMode(choice, resolvedStateHome);
546
+ persistSandboxMode(choice, opts.cwd);
464
547
  }
465
548
  catch {
466
549
  return {
467
- text: `Could not write the sandbox setting to ~/.yagni-code/config.json — edit it directly ("sandbox": { "enabled": ${choice !== "disabled"} }) and restart.`,
550
+ text: `Could not write the sandbox setting to .yagni-code/config.local.json — edit it directly ("sandbox": { "enabled": ${choice !== "disabled"} }) and restart.`,
468
551
  level: "error",
469
552
  };
470
553
  }
@@ -484,37 +567,37 @@ export function registerSandbox(pi, opts) {
484
567
  // the new mode to the model's bash tool.
485
568
  ctx.ui.setStatus?.("sandbox", "sandbox on (restart to apply)");
486
569
  return {
487
- text: `${message} — saved to user settings (~/.yagni-code/config.json); restart the session (or /new) to apply it to the model's bash tool.`,
570
+ text: `${message} — saved to project-local settings (.yagni-code/config.local.json); restart the session (or /new) to apply it to the model's bash tool.`,
488
571
  level: "info",
489
572
  };
490
573
  }
491
574
  // Already-enabled session changing autoAllow only: refresh the
492
575
  // live merge so the next bash call sees the new decision posture.
493
576
  if (manager.initialized)
494
- manager.refreshConfig(rules);
577
+ manager.refreshConfig(effectiveRulesForSandbox());
495
578
  ctx.ui.setStatus?.("sandbox", "sandbox on");
496
579
  return {
497
- text: `${message} — saved to user settings (~/.yagni-code/config.json).`,
580
+ text: `${message} — saved to project-local settings (.yagni-code/config.local.json).`,
498
581
  level: "info",
499
582
  };
500
583
  },
501
584
  onOverrideSelect: async (choice) => {
502
585
  try {
503
- persistSandboxOverride(choice, resolvedStateHome);
586
+ persistSandboxOverride(choice, opts.cwd);
504
587
  }
505
588
  catch {
506
589
  return {
507
- text: `Could not write the sandbox override to ~/.yagni-code/config.json — edit it directly (\"sandbox\": { \"allowUnsandboxedCommands\": ... }) and restart.`,
590
+ text: `Could not write the sandbox override to .yagni-code/config.local.json — edit it directly ("sandbox": { "allowUnsandboxedCommands": ... }) and restart.`,
508
591
  level: "error",
509
592
  };
510
593
  }
511
594
  currentSettings = load();
512
595
  if (manager.initialized)
513
- manager.refreshConfig(rules);
596
+ manager.refreshConfig(effectiveRulesForSandbox());
514
597
  return {
515
598
  text: choice === "open"
516
- ? "✓ Unsandboxed fallback allowed — commands can run outside the sandbox when necessary — saved to user settings"
517
- : "✓ Strict sandbox mode — all commands must run in the sandbox or be excluded via the excludedCommands option — saved to user settings",
599
+ ? "✓ Unsandboxed fallback allowed — commands can run outside the sandbox when necessary — saved to project-local settings"
600
+ : "✓ Strict sandbox mode — all commands must run in the sandbox or be excluded via the excludedCommands option — saved to project-local settings",
518
601
  level: "info",
519
602
  };
520
603
  },
@@ -580,55 +663,25 @@ export function registerSandbox(pi, opts) {
580
663
  * Persist a domain grant to the user or project sandbox config (fail-soft:
581
664
  * the session grant already applies even if the write fails).
582
665
  */
583
- /**
584
- * Shared safe config mutation for the isolated user/project config.json:
585
- * - a PARSE FAILURE aborts (never treats a corrupt/half-written file as
586
- * empty — that would wipe activeProfile/permissions);
587
- * - the write is ATOMIC (tmp file + rename) so a crash never truncates;
588
- * - symlinked targets are rejected (a hostile repo's config symlink must
589
- * not redirect grants to arbitrary paths);
590
- * - the mutator runs only on a parsed object; a thrown mutator aborts.
591
- * Throws on any failure — callers decide fail-soft messaging.
592
- */
593
- function mutateConfigJson(target, mutate) {
594
- let parsed;
595
- try {
596
- parsed = JSON.parse(readFileSync(target, "utf-8"));
597
- }
598
- catch (err) {
599
- if (err.code === "ENOENT") {
600
- parsed = {}; // genuinely fresh file — fine
601
- }
602
- else {
603
- throw new Error(`config.json at ${target} is not valid JSON — refusing to rewrite it`);
604
- }
605
- }
606
- if (!isPlainRecord(parsed))
607
- throw new Error(`config.json at ${target} is not an object`);
608
- const config = { ...parsed };
609
- mutate(config);
610
- mkdirSync(dirname(target), { recursive: true });
611
- // Reject symlinked targets: lstat must show a regular file (or absent).
612
- try {
613
- const st = lstatSync(target);
614
- if (!st.isFile())
615
- throw new Error(`${target} is not a regular file (symlink?) — refusing`);
616
- }
617
- catch (err) {
618
- if (err.code !== "ENOENT")
619
- throw err;
620
- }
621
- const tmp = `${target}.sandbox-tmp-${process.pid}`;
622
- writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
623
- // chmod: writeFile honors mode only on create — guarantee 0600 even when a
624
- // same-pid retry reuses an existing tmp file (same posture as
625
- // persistUserRule's config write).
626
- chmodSync(tmp, 0o600);
627
- renameSync(tmp, target);
628
- }
666
+ /** Local helper for the grant/target writers below (shared shape). */
629
667
  function isPlainRecord(v) {
630
668
  return typeof v === "object" && v !== null && !Array.isArray(v);
631
669
  }
670
+ /** Sink line for a failed domain-grant persist — the thrown message is
671
+ * mutateConfigJson's own path/reason text (diagnosable, no file content),
672
+ * same posture as sandbox_persist_failed / rule_save_failed. */
673
+ function logGrantPersistFailure(host, scope, err) {
674
+ logEvent({
675
+ source: "sandbox",
676
+ level: "warn",
677
+ event: "sandbox_persist_failed",
678
+ fields: {
679
+ grant: host,
680
+ scope,
681
+ error: err instanceof Error ? err.message : String(err),
682
+ },
683
+ });
684
+ }
632
685
  /** Persist a domain grant to the user or project sandbox config. */
633
686
  function persistDomainGrant(projectRoot, domain, stateHome) {
634
687
  const target = projectRoot
@@ -645,25 +698,27 @@ function persistDomainGrant(projectRoot, domain, stateHome) {
645
698
  config.sandbox = sandbox;
646
699
  });
647
700
  }
648
- /** Persist sandbox.enabled for the /sandbox enable-from-default-off path. */
649
- function persistEnabledFlip(enabled, stateHome) {
650
- const target = join(stateHome, "config.json");
651
- mutateConfigJson(target, (config) => {
701
+ /**
702
+ * Persist sandbox.enabled to the PROJECT-LOCAL config — the personal
703
+ * per-project tier, Claude Code's own destination for these writes
704
+ * (settings.local.json). Local writes fire-and-forget the global-gitignore
705
+ * helper so the file is never committed by accident.
706
+ */
707
+ function persistEnabledFlip(enabled, cwd) {
708
+ mutateLocalConfig(cwd, (config) => {
652
709
  const sandbox = isPlainRecord(config.sandbox) ? { ...config.sandbox } : {};
653
710
  sandbox.enabled = enabled;
654
711
  config.sandbox = sandbox;
655
- });
712
+ }, "sandbox");
656
713
  }
657
714
  /**
658
715
  * Persist a panel Mode choice (enabled + autoAllowBashIfSandboxed pair) to
659
- * the USER config. Claude persists these to settings.local.json (personal
660
- * per-project); our project-local tier does not exist yet, so the user
661
- * config is the interim destination — cross-project by nature, which the
662
- * confirmation message states honestly.
716
+ * the PROJECT-LOCAL config Claude Code's own panel persists its mode
717
+ * choice to settings.local.json; ours now matches. Also fire-and-forgets
718
+ * the global-gitignore helper on first write.
663
719
  */
664
- function persistSandboxMode(choice, stateHome) {
665
- const target = join(stateHome, "config.json");
666
- mutateConfigJson(target, (config) => {
720
+ function persistSandboxMode(choice, cwd) {
721
+ mutateLocalConfig(cwd, (config) => {
667
722
  const sandbox = isPlainRecord(config.sandbox) ? { ...config.sandbox } : {};
668
723
  if (choice === "auto-allow") {
669
724
  sandbox.enabled = true;
@@ -678,17 +733,16 @@ function persistSandboxMode(choice, stateHome) {
678
733
  sandbox.autoAllowBashIfSandboxed = false;
679
734
  }
680
735
  config.sandbox = sandbox;
681
- });
736
+ }, "sandbox");
682
737
  }
683
738
  /** Persist a panel Overrides choice (allowUnsandboxedCommands) — same
684
739
  * destination + honesty contract as persistSandboxMode. */
685
- function persistSandboxOverride(choice, stateHome) {
686
- const target = join(stateHome, "config.json");
687
- mutateConfigJson(target, (config) => {
740
+ function persistSandboxOverride(choice, cwd) {
741
+ mutateLocalConfig(cwd, (config) => {
688
742
  const sandbox = isPlainRecord(config.sandbox) ? { ...config.sandbox } : {};
689
743
  sandbox.allowUnsandboxedCommands = choice === "open";
690
744
  config.sandbox = sandbox;
691
- });
745
+ }, "sandbox");
692
746
  }
693
747
  /**
694
748
  * Wait for a spawned child's exit without hanging on inherited stdio —
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared settings-file plumbing for the three YAGNI Code settings tiers:
3
+ * user ~/.yagni-code/config.json (always trusted)
4
+ * project .yagni-code/config.json under the cwd (shared, committed)
5
+ * local .yagni-code/config.local.json under cwd (personal, gitignored)
6
+ *
7
+ * Owns the atomic config mutation (promoted from sandbox/session.ts, the
8
+ * single copy now) and the global-gitignore helper (Claude Code
9
+ * addFileGlobRuleToGitignore parity): on the first write to the local file,
10
+ * the LOCAL_GITIGNORE_ENTRY glob lands in the GLOBAL git ignore file so
11
+ * personal settings are never committed. Fail-soft by design — a gitignore
12
+ * failure never fails the settings write.
13
+ */
14
+ export declare function mutateConfigJson(target: string, mutate: (config: Record<string, unknown>) => void): void;
15
+ /** The project-local settings path for a session cwd. */
16
+ export declare function localConfigPath(cwd: string): string;
17
+ /**
18
+ * Mutate the project-local settings file. Every write fire-and-forgets
19
+ * ensureLocalGitignored (Claude parity: the local tier is added to the
20
+ * global git ignore on write, not on session start). The helper is
21
+ * fire-and-forget BY DESIGN — the write's success notice is not gated on
22
+ * two git spawns; when the gitignore step is skipped or fails, the sink
23
+ * warn (gitignore_skipped / gitignore_update_failed) is the designed
24
+ * signal, surfaced via the diagnostics trail.
25
+ *
26
+ * `sinkSource` attributes those sink events to the caller's surface
27
+ * ("permission-rules" for rule saves, "sandbox" for panel/toggle writes) —
28
+ * the diagnostics trail then says which surface's write failed to get
29
+ * ignore-protected, not a blanket permission-rules line for every write.
30
+ */
31
+ export declare function mutateLocalConfig(cwd: string, mutate: (config: Record<string, unknown>) => void, sinkSource?: string): void;
32
+ /**
33
+ * The global git ignore path git actually reads. Git consults
34
+ * $XDG_CONFIG_HOME/git/ignore (defaulting ~/.config/git/ignore) — honoring
35
+ * XDG is a deliberate divergence from Claude (which hardcodes ~/.config):
36
+ * writing a file git will not read when XDG is set would be a silent no-op.
37
+ */
38
+ export declare function globalGitignorePath(env?: NodeJS.ProcessEnv): string;
39
+ /**
40
+ * Ensure the LOCAL_GITIGNORE_ENTRY glob (covering config.local.json at any
41
+ * depth) is ignored by the user's GLOBAL git config so local settings are
42
+ * never committed. Skip when:
43
+ * - cwd is not inside a git repo (nothing to protect);
44
+ * - `git check-ignore` already matches (local or global patterns cover it);
45
+ * - the global ignore file already carries the literal entry.
46
+ * Any failure logs one warn sink event and returns — the settings write it
47
+ * accompanies has already succeeded and must not be walked back.
48
+ */
49
+ export declare function ensureLocalGitignored(cwd: string, env?: NodeJS.ProcessEnv, sinkSource?: string): Promise<void>;
50
+ //# sourceMappingURL=settingsFiles.d.ts.map