@kaydash9999/banker-plugins 0.13.0 → 0.15.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +82 -0
- package/README.md +50 -17
- package/codex/manifest.json +11 -8
- package/codex/transform-matrix.md +16 -4
- package/commands/front-qa.md +8 -5
- package/commands/setup.md +13 -4
- package/hooks/graceful-pause.mjs +222 -0
- package/hooks/hooks.json +2 -1
- package/hooks/register.mjs +12 -0
- package/package.json +4 -1
- package/skills/3d-intro-build/SKILL.md +1 -1
- package/skills/3d-intro-setup/SKILL.md +1 -1
- package/skills/all-in-one/SKILL.md +160 -55
- package/skills/append_wiki/SKILL.md +1 -1
- package/skills/arch-diagram/SKILL.md +1 -1
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +1 -1
- package/skills/audit-web-page/SKILL.md +1 -1
- package/skills/cleansing-memory/SKILL.md +1 -1
- package/skills/compact-copy/SKILL.md +14 -13
- package/skills/compact-wiki/SKILL.md +1 -1
- package/skills/curation/SKILL.md +36 -3
- package/skills/deep-init/SKILL.md +1 -1
- package/skills/deep-research/SKILL.md +1 -1
- package/skills/docs-setup/SKILL.md +1 -1
- package/skills/humanizer/SKILL.md +1 -1
- package/skills/interval-report/SKILL.md +1 -1
- package/skills/lineage/SKILL.md +146 -16
- package/skills/lineage/lineage.py +1091 -107
- package/skills/make-notion-guide/SKILL.md +1 -1
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-setup/SKILL.md +1 -1
- package/skills/nothing-design/SKILL.md +1 -1
- package/skills/obsidizer/SKILL.md +1 -1
- package/skills/omc-reference/SKILL.md +68 -33
- package/skills/payload-mon/SKILL.md +66 -0
- package/skills/payload-mon/scripts/payload-mon.mjs +377 -0
- package/skills/payload-mon/scripts/payload-size.mjs +238 -0
- package/skills/pdf-vision-extract/SKILL.md +1 -1
- package/skills/play-qa/SKILL.md +1 -1
- package/skills/ralph-qa/SKILL.md +263 -112
- package/skills/ralph-qa/references/gemini-read-only.toml +9 -0
- package/skills/ralph-qa/references/gemini-seat.mjs +175 -0
- package/skills/ralph-qa/references/payload-scan.mjs +130 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +872 -780
- package/skills/ready-compact/SKILL.md +114 -67
- package/skills/refresh-git-ignore/SKILL.md +1 -1
- package/skills/refresh-readme/SKILL.md +3 -3
- package/skills/rfp-author/SKILL.md +3 -3
- package/skills/setup-bypass-permissions/SKILL.md +123 -0
- package/skills/setup-bypass-permissions/agents/openai.yaml +4 -0
- package/skills/setup-bypass-permissions/scripts/bypass-permissions.mjs +381 -0
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.ps1 +247 -0
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +241 -0
- package/skills/{harness-factory → setup-harness-factory}/SKILL.md +3 -3
- package/skills/setup-insane-search/SKILL.md +1 -1
- package/skills/setup-java/SKILL.md +1 -1
- package/skills/setup-lsp/SKILL.md +1 -1
- package/skills/setup-mcp/SKILL.md +1 -1
- package/skills/setup-node/SKILL.md +1 -1
- package/skills/setup-omc/SKILL.md +11 -4
- package/skills/setup-omc-hud/SKILL.md +26 -1
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +247 -0
- package/skills/setup-playwright/SKILL.md +1 -1
- package/skills/setup-pwsh/SKILL.md +1 -1
- package/skills/setup-python/SKILL.md +1 -1
- package/skills/setup-sandbox/SKILL.md +1 -1
- package/skills/setup-stitch/SKILL.md +1 -1
- package/skills/setup-tmux/SKILL.md +1 -1
- package/skills/smart-compact/SKILL.md +10 -9
- package/skills/summary-wiki/SKILL.md +1 -1
- package/skills/tone-compact/SKILL.md +203 -0
- package/skills/tone-compact/scripts/tone-compact.mjs +499 -0
- package/skills/ultra-init/SKILL.md +53 -42
- package/skills/ultra-interview/SKILL.md +1 -1
- package/skills/ultra-ui-qa/SKILL.md +1 -1
- package/skills/update-banker/SKILL.md +1 -1
- package/skills/vertical-pptx/SKILL.md +1 -1
- package/skills/vertical-pptx-setup/SKILL.md +1 -1
- package/skills/visual-ralph/SKILL.md +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bypass-permissions: turns Claude Code's default permission mode to bypassPermissions, or back.
|
|
4
|
+
*
|
|
5
|
+
* node bypass-permissions.mjs on --yes set permissions.defaultMode to "bypassPermissions"
|
|
6
|
+
* node bypass-permissions.mjs off put back what was there before `on`
|
|
7
|
+
* node bypass-permissions.mjs [status] say what the settings, this machine's policy and this account allow
|
|
8
|
+
*
|
|
9
|
+
* `--yes` stands for the confirmation the skill asks for first: without it `on` changes nothing.
|
|
10
|
+
*
|
|
11
|
+
* The file is <config>/settings.json, where <config> is $CLAUDE_CONFIG_DIR or ~/.claude, and it
|
|
12
|
+
* must already exist as a folder: a machine without Claude Code gets nothing. Every other key stays
|
|
13
|
+
* as it was, and so does a BOM before the JSON (Claude Code reads such a file). The file is replaced
|
|
14
|
+
* whole through a temp file and a rename, keeping its mode; a link at its name stays a link.
|
|
15
|
+
* `on` remembers what it replaced in <settings>.bypass-permissions.json, written before the
|
|
16
|
+
* settings, so a stop partway never leaves a mode `off` cannot take back. `off` restores exactly
|
|
17
|
+
* that: the earlier mode, or no key and no permissions object when there was none, and drops the
|
|
18
|
+
* warning skip (skipDangerousModePermissionPrompt) Claude Code writes when the mode is first accepted,
|
|
19
|
+
* unless it was there before. A record that does not hold up is ignored.
|
|
20
|
+
* The text from before `on` is kept in <settings>.bypass-permissions.bak, readable by this account
|
|
21
|
+
* alone: settings can hold secrets.
|
|
22
|
+
* $BANKER_BYPASS_POLICY_FILES and $BANKER_BYPASS_POLICY_DIRS replace the policy paths in this skill's tests,
|
|
23
|
+
* and only with BANKER_BYPASS_TEST=1. The settings path has no override: a repository's .claude/settings.json
|
|
24
|
+
* `env` can set most variables (Claude Code ignores a list there that holds CLAUDE_CONFIG_DIR and HOME), so
|
|
25
|
+
* only those two may decide where it is.
|
|
26
|
+
* That `env` can still set BANKER_BYPASS_TEST and IS_SANDBOX, hiding the policy or the root account from
|
|
27
|
+
* this script; Claude Code then still enforces the policy itself, so the worst is a report of `on` that
|
|
28
|
+
* the next session's /status does not bear out.
|
|
29
|
+
*
|
|
30
|
+
* `on` refuses where Claude Code would not honour the mode or not start with it: this machine's
|
|
31
|
+
* managed settings or their drop-in folder forbid it (permissions.disableBypassPermissionsMode), the
|
|
32
|
+
* settings file forbids it itself, or the account is root outside a sandbox Claude Code recognises
|
|
33
|
+
* (IS_SANDBOX=1, or its bubblewrap one). It also refuses a file that is not a JSON object, and a
|
|
34
|
+
* policy file or folder this account cannot read: then nothing says the policy allows the mode.
|
|
35
|
+
*
|
|
36
|
+
* Exit status: 0 done, or nothing to do. 1 refused, nothing changed. 2 an unexpected error
|
|
37
|
+
* stopped the command partway: check `status`. 3 this account may not write the file or its
|
|
38
|
+
* folder (a sandbox, a read-only mount, a file another account owns): the settings are unchanged.
|
|
39
|
+
* Every output ends with the line `설정 파일: <path>`.
|
|
40
|
+
*/
|
|
41
|
+
import { accessSync, chmodSync, constants, existsSync, lstatSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
42
|
+
import { homedir } from "node:os";
|
|
43
|
+
import { dirname, join } from "node:path";
|
|
44
|
+
import { fileURLToPath } from "node:url";
|
|
45
|
+
|
|
46
|
+
const MODE = "bypassPermissions";
|
|
47
|
+
const CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
48
|
+
|
|
49
|
+
// Where Claude Code reads machine-wide managed settings, per OS: a file and a drop-in folder.
|
|
50
|
+
const POLICY_HOMES = {
|
|
51
|
+
linux: "/etc/claude-code",
|
|
52
|
+
darwin: "/Library/Application Support/ClaudeCode",
|
|
53
|
+
win32: "C:\\Program Files\\ClaudeCode",
|
|
54
|
+
};
|
|
55
|
+
const policyHome = POLICY_HOMES[process.platform] || POLICY_HOMES.linux;
|
|
56
|
+
const TESTING = process.env.BANKER_BYPASS_TEST === "1";
|
|
57
|
+
const testPaths = (name) => (TESTING && process.env[name] ? process.env[name].split("\n").filter(Boolean) : null);
|
|
58
|
+
|
|
59
|
+
const DEFAULTS = {
|
|
60
|
+
settingsFile: join(CONFIG_DIR, "settings.json"),
|
|
61
|
+
policyFiles: testPaths("BANKER_BYPASS_POLICY_FILES") || [join(policyHome, "managed-settings.json")],
|
|
62
|
+
policyDirs: testPaths("BANKER_BYPASS_POLICY_DIRS") || [join(policyHome, "managed-settings.d")],
|
|
63
|
+
uid: process.getuid?.(),
|
|
64
|
+
sandbox: process.env.IS_SANDBOX,
|
|
65
|
+
bubblewrap: process.env.CLAUDE_CODE_BUBBLEWRAP,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Write failures that mean "this account may not", not "something broke".
|
|
69
|
+
const DENIED = new Set(["EACCES", "EPERM", "EROFS"]);
|
|
70
|
+
// A rename that cannot land on the settings file (a bind-mounted single file): write in place.
|
|
71
|
+
const IN_PLACE = new Set(["EBUSY", "EXDEV"]);
|
|
72
|
+
const WARNING_SKIP = "skipDangerousModePermissionPrompt";
|
|
73
|
+
const MODES = new Set(["default", "manual", "acceptEdits", "plan", "auto", "dontAsk"]); // what `previous` may name
|
|
74
|
+
|
|
75
|
+
const stateOf = (settingsFile) => `${settingsFile}.bypass-permissions.json`;
|
|
76
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
77
|
+
const forbids = (v) => v === true || v === "disable";
|
|
78
|
+
const refusal = (why, file) => ({ ok: false, message: `${why}. 설정 파일을 그대로 두었습니다: ${file}` });
|
|
79
|
+
const denied = (why, file) => ({ ok: false, code: 3, message: `설정 파일에 쓸 권한이 없습니다(${why}). 설정 파일을 그대로 두었습니다: ${file}` });
|
|
80
|
+
|
|
81
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); // keeps a BOM in the text, refuses other bytes
|
|
82
|
+
|
|
83
|
+
// The file as { raw, text }, null when there is no such file, { raw, bad } when it is not UTF-8.
|
|
84
|
+
// Any other read failure propagates.
|
|
85
|
+
function readParts(file) {
|
|
86
|
+
let raw;
|
|
87
|
+
try {
|
|
88
|
+
raw = readFileSync(file);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") return null;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return { raw, text: utf8.decode(raw) };
|
|
95
|
+
} catch {
|
|
96
|
+
return { raw, bad: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const readText = (file) => readParts(file)?.text ?? null;
|
|
100
|
+
|
|
101
|
+
// parseSettings for readParts' answer.
|
|
102
|
+
const parseParts = (parts) => (parts?.bad ? { why: "설정 파일이 UTF-8 이 아닙니다" } : parseSettings(parts ? parts.text : null));
|
|
103
|
+
|
|
104
|
+
// { data, bom } for a JSON object (an absent file reads as {}), { why } for anything else.
|
|
105
|
+
function parseSettings(text) {
|
|
106
|
+
if (text === null) return { data: {}, bom: "" };
|
|
107
|
+
const bom = text.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
108
|
+
if (!text.slice(bom.length).trim()) return { data: {}, bom };
|
|
109
|
+
let data;
|
|
110
|
+
try {
|
|
111
|
+
data = JSON.parse(text.slice(bom.length));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return { why: `설정 파일이 올바른 JSON 이 아닙니다(${e.message})` };
|
|
114
|
+
}
|
|
115
|
+
if (!isObject(data)) return { why: "설정 파일의 최상위가 JSON 객체가 아닙니다" };
|
|
116
|
+
if ("permissions" in data && !isObject(data.permissions)) return { why: "설정 파일의 permissions 항목이 객체가 아닙니다" };
|
|
117
|
+
return { data, bom };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A policy path that is not there, or is not a file of that kind: no policy, not a failure.
|
|
121
|
+
const ABSENT = new Set(["ENOENT", "ENOTDIR", "EISDIR"]);
|
|
122
|
+
const unreadablePolicy = (path) => `이 머신의 관리 정책을 읽을 수 없습니다(${path}). 관리 정책이 이 모드를 허용하는지 관리자에게 확인하세요`;
|
|
123
|
+
|
|
124
|
+
// { files } of the policy drop-in folders, in the order Claude Code reads them (hidden files are
|
|
125
|
+
// not policy), or { unreadable } naming a folder that is there but cannot be listed.
|
|
126
|
+
function dropIns(dirs) {
|
|
127
|
+
const files = [];
|
|
128
|
+
for (const dir of dirs) {
|
|
129
|
+
let names;
|
|
130
|
+
try {
|
|
131
|
+
names = readdirSync(dir);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (ABSENT.has(e.code)) continue;
|
|
134
|
+
return { unreadable: dir };
|
|
135
|
+
}
|
|
136
|
+
files.push(...names.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((f) => join(dir, f)));
|
|
137
|
+
}
|
|
138
|
+
return { files };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Why this machine's policy stops the mode, or "": a managed settings file or drop-in forbids it, or
|
|
142
|
+
// a policy file or folder is there but this account cannot read it, so nothing says it allows it.
|
|
143
|
+
function policyBlock(o) {
|
|
144
|
+
const listed = dropIns(o.policyDirs);
|
|
145
|
+
if (listed.unreadable) return unreadablePolicy(listed.unreadable);
|
|
146
|
+
for (const file of [...o.policyFiles, ...listed.files]) {
|
|
147
|
+
let text;
|
|
148
|
+
try {
|
|
149
|
+
text = readFileSync(file, "utf8");
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if (ABSENT.has(e.code)) continue;
|
|
152
|
+
return unreadablePolicy(file);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
if (forbids(JSON.parse(text.replace(/^\uFEFF/, ""))?.permissions?.disableBypassPermissionsMode)) {
|
|
156
|
+
return `이 머신의 관리 정책이 bypassPermissions 를 막습니다(${file})`;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
/* not JSON: nothing this script can see forbids it */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// An environment flag as Claude Code reads it: 1, true, yes or on, any case.
|
|
166
|
+
const truthy = (v) => ["1", "true", "yes", "on"].includes(String(v ?? "").trim().toLowerCase());
|
|
167
|
+
|
|
168
|
+
// Why Claude Code would not run in the mode here, or "".
|
|
169
|
+
function modeBlock(o, data) {
|
|
170
|
+
// Claude Code lets root use the mode inside a sandbox it recognises: IS_SANDBOX exactly "1", or
|
|
171
|
+
// CLAUDE_CODE_BUBBLEWRAP set to a true value (2.1.289 reads 1, true, yes, on; 0 or false is not one).
|
|
172
|
+
if (o.uid === 0 && o.sandbox !== "1" && !truthy(o.bubblewrap)) {
|
|
173
|
+
return "root 계정에서는 Claude Code 가 bypassPermissions 로 시작하지 않습니다. 격리된 샌드박스라면 IS_SANDBOX=1 을 설정한 Claude Code 에서 다시 실행하세요";
|
|
174
|
+
}
|
|
175
|
+
const blocked = policyBlock(o);
|
|
176
|
+
if (blocked) return blocked;
|
|
177
|
+
if (forbids(data.permissions?.disableBypassPermissionsMode)) {
|
|
178
|
+
return "설정 파일 자체의 permissions.disableBypassPermissionsMode 가 bypassPermissions 를 막습니다";
|
|
179
|
+
}
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Replaces file through a fresh temp file and a rename, so a link planted at its name
|
|
184
|
+
// is replaced rather than written through. A folder in the way is not a permission problem,
|
|
185
|
+
// though Windows answers the rename with EPERM: say so on every OS, before any write.
|
|
186
|
+
function writeFresh(file, data, mode) {
|
|
187
|
+
if (lstatSync(file, { throwIfNoEntry: false })?.isDirectory()) {
|
|
188
|
+
throw Object.assign(new Error(`쓸 자리에 폴더가 있습니다: ${file}`), { code: "EISDIR" });
|
|
189
|
+
}
|
|
190
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
191
|
+
rmSync(tmp, { force: true });
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(tmp, data, { flag: "wx", mode });
|
|
194
|
+
chmodSync(tmp, mode); // the umask has no say
|
|
195
|
+
renameSync(tmp, file);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
rmSync(tmp, { force: true });
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const serialize = (data, bom) => bom + JSON.stringify(data, null, 2) + "\n";
|
|
203
|
+
|
|
204
|
+
// The answer for an existing settings file this account may not write; null when it may.
|
|
205
|
+
function writeDenial(file, before) {
|
|
206
|
+
if (before === null) return null;
|
|
207
|
+
try {
|
|
208
|
+
accessSync(file, constants.W_OK);
|
|
209
|
+
return null;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return denied(e.code || e.message, file);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Replaces the file behind a link at `file` (the link stays) whole, keeping its mode.
|
|
216
|
+
function replaceSettings(file, text) {
|
|
217
|
+
const real = existsSync(file) ? realpathSync(file) : file;
|
|
218
|
+
const mode = existsSync(real) ? statSync(real).mode & 0o777 : 0o600;
|
|
219
|
+
try {
|
|
220
|
+
writeFresh(real, text, mode);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
if (!IN_PLACE.has(e.code)) throw e;
|
|
223
|
+
writeFileSync(file, text);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Writes `text` as the settings, after keeping the old text in the .bak when `backup` asks for it.
|
|
228
|
+
function commit(o, before, text, done, backup) {
|
|
229
|
+
if (backup && before !== null) writeFresh(`${o.settingsFile}.bypass-permissions.bak`, before, 0o600);
|
|
230
|
+
replaceSettings(o.settingsFile, text);
|
|
231
|
+
return { ok: true, message: done };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Reads the settings back after a write: the mode must be what the write meant.
|
|
235
|
+
function confirmMode(o, wantOn) {
|
|
236
|
+
const { data } = parseSettings(readText(o.settingsFile));
|
|
237
|
+
if ((data?.permissions?.defaultMode === MODE) !== wantOn) throw new Error("다시 읽은 설정의 defaultMode 가 쓴 값과 다릅니다");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function turnOn(opts = {}) {
|
|
241
|
+
const o = { ...DEFAULTS, ...opts };
|
|
242
|
+
const parts = readParts(o.settingsFile);
|
|
243
|
+
const before = parts ? parts.raw : null; // the .bak keeps these bytes
|
|
244
|
+
const { data, bom, why } = parseParts(parts);
|
|
245
|
+
if (why) return refusal(why, o.settingsFile);
|
|
246
|
+
const blocked = modeBlock(o, data);
|
|
247
|
+
if (blocked) return refusal(blocked, o.settingsFile);
|
|
248
|
+
if (data.permissions?.defaultMode === MODE) return { ok: true, message: "이미 켜져 있습니다 (변경 없음)." };
|
|
249
|
+
if (!existsSync(dirname(o.settingsFile))) {
|
|
250
|
+
return refusal(`Claude Code 설정 폴더가 없습니다(${dirname(o.settingsFile)}). 이 머신에서 Claude Code 를 쓰지 않는다면 켤 것이 없습니다`, o.settingsFile);
|
|
251
|
+
}
|
|
252
|
+
const denial = writeDenial(o.settingsFile, before);
|
|
253
|
+
if (denial) return denial;
|
|
254
|
+
const hadPermissions = isObject(data.permissions);
|
|
255
|
+
const previous = hadPermissions && "defaultMode" in data.permissions ? data.permissions.defaultMode : null;
|
|
256
|
+
const record = { hadPermissions, previous, warningSkip: WARNING_SKIP in data ? { value: data[WARNING_SKIP] } : null };
|
|
257
|
+
const next = { ...data, permissions: { ...(data.permissions || {}), defaultMode: MODE } };
|
|
258
|
+
writeFresh(stateOf(o.settingsFile), JSON.stringify(record) + "\n", 0o600);
|
|
259
|
+
let res;
|
|
260
|
+
try {
|
|
261
|
+
res = commit(o, before, serialize(next, bom), "켰습니다. 다음 세션부터 도구 실행 확인 없이 진행합니다. 되돌리려면 off.", true);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
rmSync(stateOf(o.settingsFile), { force: true }); // the settings were not replaced
|
|
264
|
+
throw e;
|
|
265
|
+
}
|
|
266
|
+
// From here the record stays, even when the read-back fails: the mode may be on, and only the
|
|
267
|
+
// record lets off put back what was there. A stale one is harmless; off and on both replace it.
|
|
268
|
+
confirmMode(o, true);
|
|
269
|
+
return res;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// What `on` replaced, or null when there is no record or it does not hold up.
|
|
273
|
+
function remembered(settingsFile) {
|
|
274
|
+
let s;
|
|
275
|
+
try {
|
|
276
|
+
s = JSON.parse(readFileSync(stateOf(settingsFile), "utf8"));
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!isObject(s) || typeof s.hadPermissions !== "boolean") return null;
|
|
281
|
+
if (s.previous !== null && !MODES.has(s.previous)) return null;
|
|
282
|
+
if (s.warningSkip !== undefined && s.warningSkip !== null && !(isObject(s.warningSkip) && "value" in s.warningSkip)) return null;
|
|
283
|
+
return s;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The settings as they were before `on`, as far as the record tells.
|
|
287
|
+
function restored(data, state) {
|
|
288
|
+
const permissions = { ...data.permissions };
|
|
289
|
+
if (state && state.previous !== null) permissions.defaultMode = state.previous;
|
|
290
|
+
else delete permissions.defaultMode;
|
|
291
|
+
const next = { ...data, permissions };
|
|
292
|
+
if (state && !state.hadPermissions && Object.keys(permissions).length === 0) delete next.permissions;
|
|
293
|
+
if (state && state.warningSkip === null) delete next[WARNING_SKIP];
|
|
294
|
+
else if (state && isObject(state.warningSkip)) next[WARNING_SKIP] = state.warningSkip.value;
|
|
295
|
+
return next;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function turnOff(opts = {}) {
|
|
299
|
+
const o = { ...DEFAULTS, ...opts };
|
|
300
|
+
const parts = readParts(o.settingsFile);
|
|
301
|
+
const before = parts ? parts.raw : null;
|
|
302
|
+
const { data, bom, why } = parseParts(parts);
|
|
303
|
+
if (why) return refusal(why, o.settingsFile);
|
|
304
|
+
if (data.permissions?.defaultMode !== MODE) {
|
|
305
|
+
rmSync(stateOf(o.settingsFile), { force: true }); // a record of an `on` that no longer holds
|
|
306
|
+
return { ok: true, message: "이미 꺼져 있습니다 (변경 없음)." };
|
|
307
|
+
}
|
|
308
|
+
const denial = writeDenial(o.settingsFile, before);
|
|
309
|
+
if (denial) return denial;
|
|
310
|
+
const state = remembered(o.settingsFile);
|
|
311
|
+
const done = state
|
|
312
|
+
? "껐습니다. on 하기 전의 권한 모드로 되돌렸습니다."
|
|
313
|
+
: "껐습니다. banker 가 켠 기록이 없거나 그 기록을 믿을 수 없어 defaultMode 를 지웠습니다(Claude Code 의 기본 시작 모드).";
|
|
314
|
+
const res = commit(o, before, serialize(restored(data, state), bom), done, false);
|
|
315
|
+
confirmMode(o, false);
|
|
316
|
+
rmSync(stateOf(o.settingsFile), { force: true });
|
|
317
|
+
return res;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function describe(o, parsed) {
|
|
321
|
+
if (parsed.why) return parsed.why;
|
|
322
|
+
const mode = parsed.data.permissions?.defaultMode;
|
|
323
|
+
if (mode !== MODE) return `꺼짐 (defaultMode: ${mode === undefined ? "없음" : mode})`;
|
|
324
|
+
return `켜짐 (defaultMode: ${MODE}, ${remembered(o.settingsFile) ? "banker 가 설정" : "직접 설정"})`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function status(opts = {}) {
|
|
328
|
+
const o = { ...DEFAULTS, ...opts };
|
|
329
|
+
let parsed;
|
|
330
|
+
try {
|
|
331
|
+
parsed = parseParts(readParts(o.settingsFile));
|
|
332
|
+
} catch (e) {
|
|
333
|
+
parsed = { why: `설정 파일을 읽을 수 없음 (${e.code || e.message})` };
|
|
334
|
+
}
|
|
335
|
+
const lines = [`bypass-permissions: ${describe(o, parsed)} (${o.settingsFile})`];
|
|
336
|
+
const blocked = modeBlock(o, parsed.data || {});
|
|
337
|
+
if (blocked) lines.push(`주의: ${blocked}. 이 상태에서는 on 이 거부됩니다.`);
|
|
338
|
+
return { ok: true, message: lines.join("\n") };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// The command for the exact arguments, or null: `on` needs `--yes`, and nothing else is taken.
|
|
342
|
+
function commandFor(argv) {
|
|
343
|
+
const key = JSON.stringify(argv);
|
|
344
|
+
if (key === '["on","--yes"]') return turnOn;
|
|
345
|
+
if (key === '["off"]') return turnOff;
|
|
346
|
+
if (key === "[]" || key === '["status"]') return status;
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// { code, message } for the exact arguments: what main prints before the settings file line.
|
|
351
|
+
function outcome(argv) {
|
|
352
|
+
const run = commandFor(argv);
|
|
353
|
+
if (!run) {
|
|
354
|
+
const hint = JSON.stringify(argv) === '["on"]' ? "켜기는 확인을 거친 뒤 on --yes 로만 실행합니다. 바뀐 것은 없습니다.\n" : "";
|
|
355
|
+
return { code: 1, message: `${hint}사용법: bypass-permissions on --yes | off | status` };
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const { ok, code, message } = run();
|
|
359
|
+
return { code: ok ? 0 : code || 1, message };
|
|
360
|
+
} catch (e) {
|
|
361
|
+
if (DENIED.has(e.code)) return { code: 3, message: `설정 파일이나 그 폴더에 쓸 권한이 없습니다(${e.code}). 설정 파일은 바뀌지 않았습니다.` };
|
|
362
|
+
return { code: 2, message: `예기치 못한 오류로 중단했습니다: ${e.message}\nstatus 로 상태를 확인하세요.` };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function main(argv) {
|
|
367
|
+
const { code, message } = outcome(argv);
|
|
368
|
+
process.stdout.write(`${message}\n설정 파일: ${DEFAULTS.settingsFile}\n`);
|
|
369
|
+
return code;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Run as a command, also when reached through a symlinked path.
|
|
373
|
+
const invokedDirectly = () => {
|
|
374
|
+
try {
|
|
375
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
if (invokedDirectly()) process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# setup-bypass-permissions step 3 on Windows, for when node cannot run the main script.
|
|
2
|
+
#
|
|
3
|
+
# powershell -NoProfile -File bypass-permissions.ps1 on --yes set permissions.defaultMode = "bypassPermissions"
|
|
4
|
+
# powershell -NoProfile -File bypass-permissions.ps1 off remove that key (Claude Code's default start mode)
|
|
5
|
+
#
|
|
6
|
+
# The file is <config>\settings.json, <config> being $env:CLAUDE_CONFIG_DIR or %USERPROFILE%\.claude.
|
|
7
|
+
# Every other key stays, and so does a BOM. `on` keeps the text from before in
|
|
8
|
+
# settings.json.bypass-permissions.bak; a second `on` finds the mode set and writes nothing. Both files are
|
|
9
|
+
# replaced whole through a temp file, so a stop partway leaves the old text. Paths are taken literally:
|
|
10
|
+
# [ and ] in a folder name are not wildcards. Only the node script's `off` knows what `on` replaced;
|
|
11
|
+
# this `off` removes the key.
|
|
12
|
+
#
|
|
13
|
+
# `on` refuses (exit 1, nothing written) when managed policy (managed-settings.json or its
|
|
14
|
+
# managed-settings.d drop-ins) or the settings file itself sets permissions.disableBypassPermissionsMode,
|
|
15
|
+
# for a file that is not a JSON object, for a policy file or folder this account cannot read, and on a
|
|
16
|
+
# machine with no Claude Code config folder.
|
|
17
|
+
#
|
|
18
|
+
# Exit status: 0 done, or nothing to do. 1 refused. 2 unexpected error. 3 this account may not write the
|
|
19
|
+
# file or its folder. Every result ends with the line `settings file: <path>` on stdout.
|
|
20
|
+
# Windows PowerShell 5.1 or PowerShell 7. ASCII only: Windows PowerShell 5.1 reads a script without a BOM
|
|
21
|
+
# in the ANSI code page.
|
|
22
|
+
|
|
23
|
+
$ErrorActionPreference = 'Stop'
|
|
24
|
+
Set-StrictMode -Off
|
|
25
|
+
$Mode = 'bypassPermissions'
|
|
26
|
+
$Utf8 = New-Object System.Text.UTF8Encoding($false, $true) # no BOM, and bytes that are not UTF-8 throw instead of turning into U+FFFD
|
|
27
|
+
|
|
28
|
+
$script:SettingsPath = ''
|
|
29
|
+
|
|
30
|
+
# Claude Code reads this output through a pipe as UTF-8, but Windows PowerShell 5.1 writes the console code
|
|
31
|
+
# page there (949 on Korean Windows), garbling a non-ASCII path. Redirected streams get UTF-8; a console keeps its own.
|
|
32
|
+
$script:Out = [Console]::Out
|
|
33
|
+
$script:Err = [Console]::Error
|
|
34
|
+
$Utf8Out = New-Object System.Text.UTF8Encoding($false)
|
|
35
|
+
if ([Console]::IsOutputRedirected) { $script:Out = New-Object System.IO.StreamWriter([Console]::OpenStandardOutput(), $Utf8Out); $script:Out.AutoFlush = $true }
|
|
36
|
+
if ([Console]::IsErrorRedirected) { $script:Err = New-Object System.IO.StreamWriter([Console]::OpenStandardError(), $Utf8Out); $script:Err.AutoFlush = $true }
|
|
37
|
+
|
|
38
|
+
# Every result ends with the settings file line, so the report can name the file this acted on.
|
|
39
|
+
function Write-Out([string]$text) {
|
|
40
|
+
$script:Out.WriteLine($text)
|
|
41
|
+
$script:Out.WriteLine("settings file: $script:SettingsPath")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Stop-With([int]$code, [string]$text) {
|
|
45
|
+
$script:Err.WriteLine($text)
|
|
46
|
+
$script:Out.WriteLine("settings file: $script:SettingsPath")
|
|
47
|
+
exit $code
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Test-Denied($record) {
|
|
51
|
+
$e = $record.Exception
|
|
52
|
+
while ($null -ne $e) {
|
|
53
|
+
if ($e -is [System.UnauthorizedAccessException]) { return $true }
|
|
54
|
+
$e = $e.InnerException
|
|
55
|
+
}
|
|
56
|
+
return $false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
trap {
|
|
60
|
+
if (Test-Denied $_) { Stop-With 3 "write blocked: $($_.Exception.Message); settings.json left as it was" }
|
|
61
|
+
Stop-With 2 "unexpected error: $($_.Exception.Message); check settings.json"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Test-Forbids($data) {
|
|
65
|
+
if ($data -isnot [System.Management.Automation.PSCustomObject]) { return $false }
|
|
66
|
+
$perm = $data.PSObject.Properties['permissions']
|
|
67
|
+
if ($null -eq $perm -or $perm.Value -isnot [System.Management.Automation.PSCustomObject]) { return $false }
|
|
68
|
+
$flag = $perm.Value.PSObject.Properties['disableBypassPermissionsMode']
|
|
69
|
+
if ($null -eq $flag) { return $false }
|
|
70
|
+
return (($flag.Value -is [bool]) -and $flag.Value) -or (($flag.Value -is [string]) -and ($flag.Value -ceq 'disable'))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# The text of UTF-8 bytes without a leading BOM; bytes that are not UTF-8 throw.
|
|
74
|
+
function Read-Text([byte[]]$bytes) {
|
|
75
|
+
$text = $Utf8.GetString($bytes)
|
|
76
|
+
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
|
77
|
+
return $text
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Read-Json([string]$text) {
|
|
81
|
+
if ($text.Trim().Length -eq 0) { return [pscustomobject]@{} }
|
|
82
|
+
return ($text | ConvertFrom-Json)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Get-PolicyHome {
|
|
86
|
+
# The override exists for this skill's tests alone.
|
|
87
|
+
if ($env:BANKER_BYPASS_TEST -eq '1' -and $env:BANKER_BYPASS_POLICY_HOME) { return $env:BANKER_BYPASS_POLICY_HOME }
|
|
88
|
+
if ($IsMacOS) { return '/Library/Application Support/ClaudeCode' }
|
|
89
|
+
if ($IsLinux) { return '/etc/claude-code' }
|
|
90
|
+
return 'C:\Program Files\ClaudeCode'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Stop-Unreadable([string]$path) {
|
|
94
|
+
Stop-With 1 "refused: this machine's managed policy cannot be read ($path); ask the administrator whether it allows the mode; settings.json left as it was"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# True when the failure only says the path is not there (as opposed to: there, but this account cannot read it).
|
|
98
|
+
function Test-Missing($record) {
|
|
99
|
+
$e = $record.Exception
|
|
100
|
+
while ($null -ne $e) {
|
|
101
|
+
if ($e -is [System.IO.FileNotFoundException] -or $e -is [System.IO.DirectoryNotFoundException]) { return $true }
|
|
102
|
+
$e = $e.InnerException
|
|
103
|
+
}
|
|
104
|
+
return $false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# The policy file that forbids the mode, or ''. A policy file or folder this account cannot read is refused,
|
|
108
|
+
# also when the folder above it cannot be searched (File.Exists and Directory.Exists say false then).
|
|
109
|
+
function Get-PolicyBlock {
|
|
110
|
+
$policyHome = Get-PolicyHome
|
|
111
|
+
$files = @([System.IO.Path]::Combine($policyHome, 'managed-settings.json'))
|
|
112
|
+
$drop = [System.IO.Path]::Combine($policyHome, 'managed-settings.d')
|
|
113
|
+
$listed = @()
|
|
114
|
+
if (-not [System.IO.File]::Exists($drop)) {
|
|
115
|
+
try { $listed = [System.IO.Directory]::GetFiles($drop) } catch { if (-not (Test-Missing $_)) { Stop-Unreadable $drop } }
|
|
116
|
+
}
|
|
117
|
+
$files += @($listed | Where-Object {
|
|
118
|
+
$name = [System.IO.Path]::GetFileName($_)
|
|
119
|
+
$name.EndsWith('.json') -and -not $name.StartsWith('.')
|
|
120
|
+
} | Sort-Object)
|
|
121
|
+
foreach ($file in $files) {
|
|
122
|
+
if ([System.IO.Directory]::Exists($file)) { continue }
|
|
123
|
+
$bytes = $null
|
|
124
|
+
try { $bytes = [System.IO.File]::ReadAllBytes($file) } catch { if (-not (Test-Missing $_)) { Stop-Unreadable $file } }
|
|
125
|
+
if ($null -eq $bytes) { continue }
|
|
126
|
+
try { $policy = Read-Json (Read-Text $bytes) } catch { continue }
|
|
127
|
+
if (Test-Forbids $policy) { return $file }
|
|
128
|
+
}
|
|
129
|
+
return ''
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# @{ Data; Raw } for a JSON object; Raw is $null when there is no file. Anything else is refused.
|
|
133
|
+
function Read-Settings([string]$path) {
|
|
134
|
+
if (-not [System.IO.File]::Exists($path)) { return @{ Data = [pscustomobject]@{}; Raw = $null } }
|
|
135
|
+
$raw = [System.IO.File]::ReadAllBytes($path)
|
|
136
|
+
try { $text = Read-Text $raw } catch { Stop-With 1 'refused: settings.json is not valid UTF-8; settings.json left as it was' }
|
|
137
|
+
try { $data = Read-Json $text } catch { Stop-With 1 "refused: settings.json is not valid JSON ($($_.Exception.Message)); settings.json left as it was" }
|
|
138
|
+
if ($data -isnot [System.Management.Automation.PSCustomObject]) { Stop-With 1 'refused: settings.json is not a JSON object; settings.json left as it was' }
|
|
139
|
+
$perm = $data.PSObject.Properties['permissions']
|
|
140
|
+
if ($null -ne $perm -and $perm.Value -isnot [System.Management.Automation.PSCustomObject]) {
|
|
141
|
+
Stop-With 1 'refused: the permissions entry of settings.json is not an object; settings.json left as it was'
|
|
142
|
+
}
|
|
143
|
+
return @{ Data = $data; Raw = $raw }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Test-Link([string]$path) {
|
|
147
|
+
$item = Get-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
|
|
148
|
+
return ($null -ne $item) -and ($item.LinkType -in 'SymbolicLink', 'Junction')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# The file a chain of links at path ends at (40 hops at most), or path itself.
|
|
152
|
+
function Resolve-Real([string]$path) {
|
|
153
|
+
for ($hop = 0; $hop -lt 40 -and (Test-Link $path); $hop++) {
|
|
154
|
+
$target = @((Get-Item -LiteralPath $path -Force).Target)[0]
|
|
155
|
+
if (-not [System.IO.Path]::IsPathRooted($target)) {
|
|
156
|
+
$target = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($path), $target)
|
|
157
|
+
}
|
|
158
|
+
$path = [System.IO.Path]::GetFullPath($target)
|
|
159
|
+
}
|
|
160
|
+
return $path
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# On macOS and Linux (PowerShell 7.3+), give tmp the mode of like, or 0600; Windows keeps the folder's ACL.
|
|
164
|
+
function Set-Mode([string]$tmp, [string]$like, [bool]$private) {
|
|
165
|
+
if (-not ($IsLinux -or $IsMacOS)) { return }
|
|
166
|
+
if ($null -eq [System.IO.File].GetMethod('SetUnixFileMode', [type[]]@([string], [System.IO.UnixFileMode]))) { return }
|
|
167
|
+
$mode = [System.IO.UnixFileMode]'UserRead, UserWrite'
|
|
168
|
+
if (-not $private -and [System.IO.File]::Exists($like)) { $mode = [System.IO.File]::GetUnixFileMode($like) }
|
|
169
|
+
[System.IO.File]::SetUnixFileMode($tmp, $mode)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Writes bytes to path through a temp file in the same folder. A link at path is replaced, not followed.
|
|
173
|
+
# [NullString]::Value: PowerShell turns $null into "" for a .NET string parameter, and Replace refuses "".
|
|
174
|
+
function Write-Whole([string]$path, [byte[]]$bytes, [bool]$private) {
|
|
175
|
+
$full = [System.IO.Path]::GetFullPath($path)
|
|
176
|
+
$tmp = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($full), '.bypass-permissions.' + [guid]::NewGuid().ToString('N') + '.tmp')
|
|
177
|
+
try {
|
|
178
|
+
[System.IO.File]::WriteAllBytes($tmp, $bytes)
|
|
179
|
+
Set-Mode $tmp $full $private
|
|
180
|
+
if (Test-Link $full) { [System.IO.File]::Delete($full) }
|
|
181
|
+
if ([System.IO.File]::Exists($full)) { [System.IO.File]::Replace($tmp, $full, [NullString]::Value) } else { [System.IO.File]::Move($tmp, $full) }
|
|
182
|
+
} catch {
|
|
183
|
+
if ([System.IO.File]::Exists($tmp)) { [System.IO.File]::Delete($tmp) }
|
|
184
|
+
throw
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function Save-Settings([string]$path, $data, $raw) {
|
|
189
|
+
$bytes = $Utf8.GetBytes(($data | ConvertTo-Json -Depth 64) + "`n")
|
|
190
|
+
if ($null -ne $raw -and $raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF) {
|
|
191
|
+
$bytes = [byte[]](@([byte]0xEF, [byte]0xBB, [byte]0xBF) + $bytes)
|
|
192
|
+
}
|
|
193
|
+
Write-Whole (Resolve-Real $path) $bytes $false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Drop the node script's record of what `on` replaced. This fallback keeps no record, so one left by an earlier
|
|
197
|
+
# node `on` is stale: the mode is no longer what it describes, and a later node `off` would restore it.
|
|
198
|
+
function Remove-Record([string]$path) { [System.IO.File]::Delete($path + '.bypass-permissions.json') }
|
|
199
|
+
|
|
200
|
+
function Get-Mode($data) {
|
|
201
|
+
$perm = $data.PSObject.Properties['permissions']
|
|
202
|
+
if ($null -eq $perm -or $null -eq $perm.Value.PSObject.Properties['defaultMode']) { return $null }
|
|
203
|
+
return $perm.Value.defaultMode
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function Enable-Mode([string]$path) {
|
|
207
|
+
$config = [System.IO.Path]::GetDirectoryName($path)
|
|
208
|
+
if (-not [System.IO.Directory]::Exists($config)) { Stop-With 1 "refused: there is no Claude Code config folder at $config; settings.json left as it was" }
|
|
209
|
+
$settings = Read-Settings $path
|
|
210
|
+
$blocked = Get-PolicyBlock
|
|
211
|
+
if ($blocked) { Stop-With 1 "refused: managed policy forbids bypassPermissions: $blocked; settings.json left as it was" }
|
|
212
|
+
if (Test-Forbids $settings.Data) { Stop-With 1 'refused: settings.json itself sets permissions.disableBypassPermissionsMode; settings.json left as it was' }
|
|
213
|
+
if ((Get-Mode $settings.Data) -ceq $Mode) { Write-Out "defaultMode = $Mode (already, nothing changed)"; exit 0 }
|
|
214
|
+
if ($null -ne $settings.Raw) {
|
|
215
|
+
[System.IO.File]::Open($path, 'Open', 'ReadWrite').Dispose()
|
|
216
|
+
Write-Whole ($path + '.bypass-permissions.bak') $settings.Raw $true
|
|
217
|
+
}
|
|
218
|
+
Remove-Record $path # the mode is not on, so any record is stale
|
|
219
|
+
if ($null -eq $settings.Data.PSObject.Properties['permissions']) {
|
|
220
|
+
$settings.Data | Add-Member -NotePropertyName permissions -NotePropertyValue ([pscustomobject]@{})
|
|
221
|
+
}
|
|
222
|
+
$settings.Data.permissions | Add-Member -NotePropertyName defaultMode -NotePropertyValue $Mode -Force
|
|
223
|
+
Save-Settings $path $settings.Data $settings.Raw
|
|
224
|
+
Write-Out "defaultMode = $Mode"
|
|
225
|
+
exit 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function Disable-Mode([string]$path) {
|
|
229
|
+
$settings = Read-Settings $path
|
|
230
|
+
$current = Get-Mode $settings.Data
|
|
231
|
+
if ($current -cne $Mode) { Write-Out "defaultMode = $current (not bypassPermissions, nothing changed)"; exit 0 }
|
|
232
|
+
[System.IO.File]::Open($path, 'Open', 'ReadWrite').Dispose() # a read-only settings file is a lock: off honours it as on does
|
|
233
|
+
[void]$settings.Data.permissions.PSObject.Properties.Remove('defaultMode')
|
|
234
|
+
Save-Settings $path $settings.Data $settings.Raw
|
|
235
|
+
Remove-Record $path # only after the write: if it failed, the record is still the way back
|
|
236
|
+
Write-Out "defaultMode removed (Claude Code's default start mode)"
|
|
237
|
+
exit 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
$userHome = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME }
|
|
241
|
+
$configDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { [System.IO.Path]::Combine($userHome, '.claude') }
|
|
242
|
+
$settingsPath = [System.IO.Path]::Combine($configDir, 'settings.json')
|
|
243
|
+
$script:SettingsPath = $settingsPath
|
|
244
|
+
$request = $args -join ' '
|
|
245
|
+
if ($request -ceq 'on --yes') { Enable-Mode $settingsPath }
|
|
246
|
+
if ($request -ceq 'off') { Disable-Mode $settingsPath }
|
|
247
|
+
Stop-With 1 "refused: usage is 'bypass-permissions.ps1 on --yes' or 'bypass-permissions.ps1 off'"
|