@join-pantheon/cli 0.31.0 → 0.31.2-rc.1
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 +4 -2
- package/dist/agent-outage.js +87 -0
- package/dist/agents.js +59 -20
- package/dist/claude-confine.js +21 -4
- package/dist/cli.js +260 -57
- package/dist/client.js +3 -3
- package/dist/config.js +1 -0
- package/dist/dbus.js +418 -0
- package/dist/decision-ui.js +89 -0
- package/dist/doctor.js +3 -3
- package/dist/email.js +21 -0
- package/dist/file-lock.js +3 -3
- package/dist/git-safe.js +274 -0
- package/dist/hooks-install.js +6 -1
- package/dist/install.js +21 -0
- package/dist/kiro-files.js +20 -4
- package/dist/mcp-instructions.js +2 -2
- package/dist/nonces.js +5 -3
- package/dist/notice.js +2 -1
- package/dist/notifier.js +36 -1
- package/dist/onboard-defaults.js +18 -0
- package/dist/persona.js +28 -2
- package/dist/plans.js +12 -3
- package/dist/profile.js +23 -5
- package/dist/relay.js +61 -16
- package/dist/roster-file.js +3 -0
- package/dist/run-failure.js +2 -1
- package/dist/safe-write.js +55 -0
- package/dist/setup-guide.js +9 -1
- package/dist/spend.js +65 -5
- package/dist/tray.js +53 -6
- package/dist/worker.js +890 -280
- package/helper/macos/menubar.swift +375 -112
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,8 +127,10 @@ Need another (a third person, or your own second machine)? `pantheon invite`.
|
|
|
127
127
|
|
|
128
128
|
### Plans on the hosted relay
|
|
129
129
|
|
|
130
|
-
A free account creates
|
|
131
|
-
|
|
130
|
+
A free account creates up to **three spaces**, with **two people** in each, and can have three
|
|
131
|
+
invite links open per space. A link is not a seat: if a third person uses one, they are told the
|
|
132
|
+
team is full and who can upgrade it, the owner gets an email, and the same link works once there is
|
|
133
|
+
room. Personas, messages and history are not limited. Joining spaces other people created is not limited either,
|
|
132
134
|
so a colleague on a bigger plan can always invite you. When you hit a limit the command says so
|
|
133
135
|
and what to do; `pantheon status` shows your plan and how much of it you are using. Need more?
|
|
134
136
|
[joinpantheon.network](https://www.joinpantheon.network). A space you founded and no longer need
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const MIN = 60_000, HOUR = 60 * MIN;
|
|
2
|
+
export const MAX_TEXT_WAIT_MS = 6 * HOUR;
|
|
3
|
+
const TAIL_LINES = 5;
|
|
4
|
+
const PLAN = "its organization has disabled subscription access for it";
|
|
5
|
+
const LIMIT = "its usage limit is reached";
|
|
6
|
+
const SIGNIN = "its sign-in was refused";
|
|
7
|
+
const OVERLOAD = "its service is overloaded";
|
|
8
|
+
const KINDS = {
|
|
9
|
+
"claude-code": [
|
|
10
|
+
{ re: /disabled Claude subscription access/i, why: PLAN, wait: 6 * HOUR },
|
|
11
|
+
{ re: /Claude AI usage limit reached|(?:5-hour|weekly|Opus|session) limit reached|you(?:'|’)ve hit your (?:usage )?limit|out of extra usage|credit balance is too low/i, why: LIMIT, wait: HOUR },
|
|
12
|
+
{ re: /OAuth token has expired|Invalid API key · Please run \/login|Please run \/login/i, why: SIGNIN, wait: HOUR },
|
|
13
|
+
{ re: /overloaded_error/i, why: OVERLOAD, wait: 15 * MIN },
|
|
14
|
+
],
|
|
15
|
+
codex: [
|
|
16
|
+
{ re: /you(?:'|’)ve hit your usage limit|exceeded retry limit, last status: 429|insufficient_quota/i, why: LIMIT, wait: HOUR },
|
|
17
|
+
],
|
|
18
|
+
cursor: [
|
|
19
|
+
{ re: /you(?:'|’)ve hit your usage limit|usage limit (?:reached|exceeded) for|out of (?:fast )?requests/i, why: LIMIT, wait: HOUR },
|
|
20
|
+
],
|
|
21
|
+
copilot: [
|
|
22
|
+
{ re: /you(?:'|’)ve hit a rate limit|exceeded your (?:monthly )?(?:premium requests|copilot) (?:quota|allowance|limit)/i, why: LIMIT, wait: HOUR },
|
|
23
|
+
],
|
|
24
|
+
kiro: [],
|
|
25
|
+
};
|
|
26
|
+
function namedTime(line, now) {
|
|
27
|
+
const rel = /try again in\s+(\d{1,3})\s*(minute|min|hour|hr|day)s?\b/i.exec(line);
|
|
28
|
+
if (rel) {
|
|
29
|
+
const n = Number(rel[1]);
|
|
30
|
+
const u = rel[2].toLowerCase();
|
|
31
|
+
return now + n * (u.startsWith("d") ? 24 * HOUR : u.startsWith("h") ? HOUR : MIN);
|
|
32
|
+
}
|
|
33
|
+
const m = /(?:resets?|reset at|try again (?:at|after))\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i.exec(line);
|
|
34
|
+
if (!m)
|
|
35
|
+
return null;
|
|
36
|
+
let h = Number(m[1]);
|
|
37
|
+
const min = m[2] ? Number(m[2]) : 0;
|
|
38
|
+
const ap = m[3]?.toLowerCase();
|
|
39
|
+
if (ap === "pm" && h < 12)
|
|
40
|
+
h += 12;
|
|
41
|
+
if (ap === "am" && h === 12)
|
|
42
|
+
h = 0;
|
|
43
|
+
if (h > 23 || min > 59 || (!ap && !m[2]))
|
|
44
|
+
return null;
|
|
45
|
+
const d = new Date(now);
|
|
46
|
+
d.setHours(h, min, 0, 0);
|
|
47
|
+
let t = d.getTime();
|
|
48
|
+
if (t <= now)
|
|
49
|
+
t += 24 * HOUR;
|
|
50
|
+
return t;
|
|
51
|
+
}
|
|
52
|
+
export function detectOutage(agent, tail, now = Date.now()) {
|
|
53
|
+
const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean).slice(-TAIL_LINES);
|
|
54
|
+
for (const k of KINDS[agent] ?? []) {
|
|
55
|
+
const line = lines.find((l) => k.re.test(l));
|
|
56
|
+
if (!line)
|
|
57
|
+
continue;
|
|
58
|
+
const epoch = agent === "claude-code" ? /Claude AI usage limit reached\|(\d{10})\b/i.exec(line) : null;
|
|
59
|
+
const named = epoch ? Number(epoch[1]) * 1000 : namedTime(line, now);
|
|
60
|
+
const until = named && named > now ? Math.min(named, now + MAX_TEXT_WAIT_MS) : now + k.wait;
|
|
61
|
+
return { why: k.why, until: Math.max(until, now + 5 * MIN) };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
export function whenAgain(until, now = Date.now()) {
|
|
66
|
+
const d = new Date(until);
|
|
67
|
+
const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
68
|
+
return new Date(now).toDateString() === d.toDateString() ? hm : `${d.toLocaleDateString("en-GB", { weekday: "short" })} ${hm}`;
|
|
69
|
+
}
|
|
70
|
+
export function activeOutages(all, now = Date.now()) {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [id, o] of all) {
|
|
73
|
+
if (o.until > now)
|
|
74
|
+
out[id] = o;
|
|
75
|
+
else
|
|
76
|
+
all.delete(id);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
export function earliest(outages) {
|
|
81
|
+
let best;
|
|
82
|
+
for (const o of Object.values(outages))
|
|
83
|
+
if (o && (!best || o.until < best.until))
|
|
84
|
+
best = o;
|
|
85
|
+
return best;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=agent-outage.js.map
|
package/dist/agents.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync } from "node:fs";
|
|
4
4
|
import { constants as osConstants, homedir, platform } from "node:os";
|
|
5
|
-
import { join, dirname, isAbsolute } from "node:path";
|
|
5
|
+
import { join, dirname, isAbsolute, relative } from "node:path";
|
|
6
|
+
import { copyInside, mkdirInside, writeInside } from "./safe-write.js";
|
|
7
|
+
import { gitCallEnv, safeGitArgs } from "./git-safe.js";
|
|
6
8
|
import { launcher, kiroDir } from "./install.js";
|
|
7
9
|
import { agentSpawn, which } from "./which.js";
|
|
10
|
+
import { earliest, whenAgain } from "./agent-outage.js";
|
|
8
11
|
import { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
|
|
9
12
|
import { claudeRunSettings, NO_CLAUDE_FEATURES, runTmpDir, sandboxCacheEnv, versionAtLeast } from "./claude-confine.js";
|
|
10
13
|
export function launchArgs(agent, prompt, name) {
|
|
@@ -211,7 +214,8 @@ export function chooseAgent(preferred, opts = {}) {
|
|
|
211
214
|
const hit = available.find((a) => a.backend.id === backend.id);
|
|
212
215
|
if (!hit)
|
|
213
216
|
throw new Error(`${backend.label} is not on PATH (looked for ${backend.binaries.join(", ")})`);
|
|
214
|
-
|
|
217
|
+
const out = opts.outages?.[backend.id];
|
|
218
|
+
return { backend, binary: hit.binary, signIn: state(backend, hit.binary), because: "pinned", skipped: [], ...(out ? { outage: out } : {}) };
|
|
215
219
|
}
|
|
216
220
|
if (!available.length)
|
|
217
221
|
throw new Error("no agent CLI on PATH — install claude, codex, cursor-agent, copilot, or kiro-cli");
|
|
@@ -239,12 +243,29 @@ export function chooseAgent(preferred, opts = {}) {
|
|
|
239
243
|
order.splice(0, order.length, ...boxed);
|
|
240
244
|
}
|
|
241
245
|
for (const c of order) {
|
|
246
|
+
const out = opts.outages?.[c.backend.id];
|
|
247
|
+
if (out) {
|
|
248
|
+
skipped.push({ backend: c.backend, why: `unavailable until ${whenAgain(out.until)} — ${out.why}` });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
242
251
|
const s = state(c.backend, c.binary);
|
|
243
252
|
if (!s || s.ok || s.unknown)
|
|
244
253
|
return { backend: c.backend, binary: c.binary, ...(s ? { signIn: s } : {}), because: c.because, skipped };
|
|
245
254
|
skipped.push({ backend: c.backend, why: `${s.detail} — ${SIGN_IN_CHECK[c.backend.id].fix}` });
|
|
246
255
|
}
|
|
247
|
-
const
|
|
256
|
+
const outs = {};
|
|
257
|
+
for (const o of order) {
|
|
258
|
+
const x = opts.outages?.[o.backend.id];
|
|
259
|
+
if (x)
|
|
260
|
+
outs[o.backend.id] = x;
|
|
261
|
+
}
|
|
262
|
+
const soonest = earliest(outs);
|
|
263
|
+
if (soonest) {
|
|
264
|
+
const first = order.find((o) => outs[o.backend.id] === soonest);
|
|
265
|
+
return { backend: first.backend, binary: first.binary, because: `${first.because} (every agent here is unavailable for now)`, skipped, outage: soonest };
|
|
266
|
+
}
|
|
267
|
+
const live = order;
|
|
268
|
+
const first = live.find((o) => !NEEDS_SIGN_IN_BEFORE_SPAWN.has(o.backend.id)) ?? live[0];
|
|
248
269
|
return { backend: first.backend, binary: first.binary, signIn: state(first.backend, first.binary), because: `${first.because} (none signed in)`, skipped };
|
|
249
270
|
}
|
|
250
271
|
export function unattendedRefusal(c, o = {}) {
|
|
@@ -371,7 +392,7 @@ export function repoCommands(worktree, rev) {
|
|
|
371
392
|
return { ok: [], refused: [] };
|
|
372
393
|
let bytes;
|
|
373
394
|
try {
|
|
374
|
-
bytes = execFileSync("git", ["-C", worktree, "show", `${rev}:${REPO_COMMANDS_FILE}`], { stdio: ["ignore", "pipe", "ignore"], timeout: 10_000, maxBuffer: 1 << 20 });
|
|
395
|
+
bytes = execFileSync("git", safeGitArgs(worktree, ["-C", worktree, "show", `${rev}:${REPO_COMMANDS_FILE}`]), { stdio: ["ignore", "pipe", "ignore"], env: gitCallEnv(["show"], process.env), timeout: 10_000, maxBuffer: 1 << 20 });
|
|
375
396
|
}
|
|
376
397
|
catch {
|
|
377
398
|
return { ok: [], refused: [] };
|
|
@@ -451,6 +472,7 @@ export function headlessSpawn(opts) {
|
|
|
451
472
|
...(opts.readOnlyDirs?.length ? { readOnlyDirs: opts.readOnlyDirs } : {}),
|
|
452
473
|
runTmp, bashRules, version: f.version, ...(opts.claude.home ? { home: opts.claude.home } : {}),
|
|
453
474
|
...(opts.os ? { os: opts.os } : {}), ...(opts.claude.userSettings ? { userSettings: opts.claude.userSettings } : {}),
|
|
475
|
+
...(opts.claude.gitCommon ? { gitCommon: opts.claude.gitCommon } : {}),
|
|
454
476
|
});
|
|
455
477
|
if (f.permissionPrompts)
|
|
456
478
|
extra.push("--permission-prompts", "none");
|
|
@@ -629,16 +651,28 @@ export function tomlInlineTable(kv, literal = false) {
|
|
|
629
651
|
export function tomlLiteral(v) {
|
|
630
652
|
return /['\r\n]/.test(v) ? JSON.stringify(v) : `'${v}'`;
|
|
631
653
|
}
|
|
632
|
-
export function materializeSpawnFiles(worktree, spawn) {
|
|
654
|
+
export function materializeSpawnFiles(worktree, spawn, runHome) {
|
|
655
|
+
const rootOf = (p) => {
|
|
656
|
+
for (const r of [runHome, worktree]) {
|
|
657
|
+
if (!r)
|
|
658
|
+
continue;
|
|
659
|
+
const rel = relative(r, p);
|
|
660
|
+
if (rel && !rel.startsWith("..") && !isAbsolute(rel))
|
|
661
|
+
return r;
|
|
662
|
+
}
|
|
663
|
+
if (!runHome && isAbsolute(p)) {
|
|
664
|
+
mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
|
|
665
|
+
return dirname(p);
|
|
666
|
+
}
|
|
667
|
+
throw new Error(`${p} is neither in the worktree nor in the run's home — not written`);
|
|
668
|
+
};
|
|
633
669
|
for (const f of spawn.writeFiles ?? []) {
|
|
634
670
|
const p = isAbsolute(f.path) ? f.path : join(worktree, f.path);
|
|
635
|
-
|
|
636
|
-
writeFileSync(p, f.contents, { mode: 0o600 });
|
|
671
|
+
writeInside(rootOf(p), p, f.contents, 0o600);
|
|
637
672
|
}
|
|
638
673
|
for (const c of spawn.copyFiles ?? []) {
|
|
639
674
|
try {
|
|
640
|
-
|
|
641
|
-
copyFileSync(c.from, c.to);
|
|
675
|
+
copyInside(rootOf(c.to), c.from, c.to, 0o600);
|
|
642
676
|
}
|
|
643
677
|
catch { }
|
|
644
678
|
}
|
|
@@ -690,7 +724,7 @@ export function hashPath(p) {
|
|
|
690
724
|
}
|
|
691
725
|
function headOf(worktree) {
|
|
692
726
|
try {
|
|
693
|
-
return execFileSync("git", ["-C", worktree, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim();
|
|
727
|
+
return execFileSync("git", safeGitArgs(worktree, ["-C", worktree, "rev-parse", "HEAD"]), { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], env: gitCallEnv(["rev-parse"], process.env), timeout: 10_000 }).trim();
|
|
694
728
|
}
|
|
695
729
|
catch {
|
|
696
730
|
return "";
|
|
@@ -768,11 +802,11 @@ function freeName(base) {
|
|
|
768
802
|
if (!taken(`${base}-${i}`))
|
|
769
803
|
return `${base}-${i}`;
|
|
770
804
|
}
|
|
771
|
-
const writeManifest = (dir, m) =>
|
|
805
|
+
const writeManifest = (dir, m) => writeInside(dir, join(dir, "manifest.json"), JSON.stringify(m), 0o600);
|
|
772
806
|
function keepDiscarded(stashRoot, dir, rel) {
|
|
773
807
|
const to = join(stashRoot, "quarantine-kept", `${Date.now()}-${randomBytes(3).toString("hex")}`, rel);
|
|
774
808
|
try {
|
|
775
|
-
|
|
809
|
+
mkdirInside(stashRoot, dirname(to));
|
|
776
810
|
moveSync(join(dir, "files", rel), to);
|
|
777
811
|
return to;
|
|
778
812
|
}
|
|
@@ -826,10 +860,15 @@ function restoreFrom(stashRoot, dir, m, mode) {
|
|
|
826
860
|
r.lines.push(`${e.rel}: could not be put back (${err.message.split("\n")[0]}); it stays held for the next run`);
|
|
827
861
|
}
|
|
828
862
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
863
|
+
try {
|
|
864
|
+
if (left.length)
|
|
865
|
+
writeManifest(dir, { ...m, moved: left });
|
|
866
|
+
else
|
|
867
|
+
rmSync(dir, { recursive: true, force: true });
|
|
868
|
+
}
|
|
869
|
+
catch (err) {
|
|
870
|
+
r.lines.push(`the record of what is still held could not be written (${err.message.split("\n")[0]}); the held files are in ${dir}`);
|
|
871
|
+
}
|
|
833
872
|
return r;
|
|
834
873
|
}
|
|
835
874
|
function readManifest(file) {
|
|
@@ -850,7 +889,7 @@ export function recoverRepoAgentConfig(worktree, stashRoot) {
|
|
|
850
889
|
if (existsSync(join(dir, "manifest.json"))) {
|
|
851
890
|
const kept = join(stashRoot, "quarantine-kept", `${Date.now()}-legacy`);
|
|
852
891
|
try {
|
|
853
|
-
|
|
892
|
+
mkdirInside(stashRoot, dirname(kept));
|
|
854
893
|
moveSync(dir, kept);
|
|
855
894
|
}
|
|
856
895
|
catch { }
|
|
@@ -942,7 +981,7 @@ export function quarantineRepoAgentConfig(worktree, stashRoot) {
|
|
|
942
981
|
const recovered = recoverRepoAgentConfig(worktree, stashRoot);
|
|
943
982
|
const dir = quarantineDir(stashRoot, worktree);
|
|
944
983
|
const m = { v: 2, worktree, head: headOf(worktree), moved: [], wrote: {} };
|
|
945
|
-
|
|
984
|
+
mkdirInside(stashRoot, join(dir, "files"));
|
|
946
985
|
let done = null;
|
|
947
986
|
const restore = () => {
|
|
948
987
|
if (done)
|
|
@@ -962,7 +1001,7 @@ export function quarantineRepoAgentConfig(worktree, stashRoot) {
|
|
|
962
1001
|
continue;
|
|
963
1002
|
const hash = hashPath(join(worktree, unit));
|
|
964
1003
|
const to = join(dir, "files", unit);
|
|
965
|
-
|
|
1004
|
+
mkdirInside(stashRoot, dirname(to));
|
|
966
1005
|
moveSync(join(worktree, unit), to);
|
|
967
1006
|
m.moved.push({ rel: unit, hash });
|
|
968
1007
|
writeManifest(dir, m);
|
package/dist/claude-confine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { lstatSync, mkdirSync, readdirSync, readFileSync, statSync
|
|
3
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { writeInside } from "./safe-write.js";
|
|
4
5
|
import { homedir, platform, tmpdir } from "node:os";
|
|
5
6
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
7
|
import { agentSpawn } from "./which.js";
|
|
@@ -97,6 +98,17 @@ export const GIT_DENY_RULES = [
|
|
|
97
98
|
"Bash(git push --delete*)", "Bash(git push * --delete*)", "Bash(git push -d *)", "Bash(git push * -d *)",
|
|
98
99
|
"Bash(git push --mirror*)", "Bash(git push * --mirror*)", "Bash(git push --all*)", "Bash(git push * --all*)",
|
|
99
100
|
];
|
|
101
|
+
export function gitDataDeny(common) {
|
|
102
|
+
const c = resolve(common);
|
|
103
|
+
const files = [join(c, "config"), join(c, "commondir"), join(c, "objects", "info", "alternates")];
|
|
104
|
+
try {
|
|
105
|
+
for (const w of readdirSync(join(c, "worktrees")))
|
|
106
|
+
for (const f of ["config.worktree", "commondir", "gitdir"])
|
|
107
|
+
files.push(join(c, "worktrees", w, f));
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
return { files, dirs: [join(c, "hooks"), join(c, "info")] };
|
|
111
|
+
}
|
|
100
112
|
const inside = (child, parent) => {
|
|
101
113
|
const r = relative(parent, child);
|
|
102
114
|
return r === "" || (!!r && !r.startsWith("..") && !isAbsolute(r));
|
|
@@ -175,6 +187,7 @@ export function claudeRunSettings(i) {
|
|
|
175
187
|
return !bad;
|
|
176
188
|
});
|
|
177
189
|
const keeps = [worktree, ...readOnly];
|
|
190
|
+
const gitDeny = i.gitCommon ? gitDataDeny(i.gitCommon) : null;
|
|
178
191
|
for (const t of targets) {
|
|
179
192
|
if (seen.has(t.abs))
|
|
180
193
|
continue;
|
|
@@ -200,6 +213,11 @@ export function claudeRunSettings(i) {
|
|
|
200
213
|
return [`Read(${p})`, `Edit(${p})`];
|
|
201
214
|
}),
|
|
202
215
|
...readOnly.flatMap((d) => [`Edit(${ruleSpelling(d, home, os)}/**)`, ...SECRET_FILE_GLOBS.map((g) => `Read(${ruleSpelling(d, home, os)}/**/${g})`)]),
|
|
216
|
+
...(gitDeny ? [
|
|
217
|
+
...gitDeny.dirs.map((d) => `Edit(${ruleSpelling(d, home, os)}/**)`),
|
|
218
|
+
...gitDeny.files.map((f) => `Edit(${ruleSpelling(f, home, os)})`),
|
|
219
|
+
`Edit(${ruleSpelling(join(resolve(i.gitCommon), "worktrees"), home, os)}/*/config.worktree)`,
|
|
220
|
+
] : []),
|
|
203
221
|
];
|
|
204
222
|
if (!i.allowBash) {
|
|
205
223
|
const permissions = { deny: ["Bash", "PowerShell", ...fileRules] };
|
|
@@ -230,7 +248,7 @@ export function claudeRunSettings(i) {
|
|
|
230
248
|
allowUnsandboxedCommands: false,
|
|
231
249
|
filesystem: {
|
|
232
250
|
allowWrite: [...new Set([resolve(i.runTmp), resolve(tmpdir())])],
|
|
233
|
-
denyWrite: readOnly.map((d) => sandboxSpelling(d, home)),
|
|
251
|
+
denyWrite: [...readOnly.map((d) => sandboxSpelling(d, home)), ...(gitDeny ? [...gitDeny.dirs, ...gitDeny.files].map((p) => sandboxSpelling(p, home)) : [])],
|
|
234
252
|
denyRead: denied.map((t) => sandboxSpelling(t.abs, home)),
|
|
235
253
|
allowRead: [hooks],
|
|
236
254
|
},
|
|
@@ -253,8 +271,7 @@ export function runTmpDir(runHome) {
|
|
|
253
271
|
if (!/^[0-9a-f]{32}$/.test(salt)) {
|
|
254
272
|
salt = randomBytes(16).toString("hex");
|
|
255
273
|
try {
|
|
256
|
-
|
|
257
|
-
writeFileSync(saltFile, salt, { mode: 0o600 });
|
|
274
|
+
writeInside(runHome, saltFile, salt, 0o600);
|
|
258
275
|
}
|
|
259
276
|
catch { }
|
|
260
277
|
}
|