@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.
Files changed (82) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +82 -0
  4. package/README.md +50 -17
  5. package/codex/manifest.json +11 -8
  6. package/codex/transform-matrix.md +16 -4
  7. package/commands/front-qa.md +8 -5
  8. package/commands/setup.md +13 -4
  9. package/hooks/graceful-pause.mjs +222 -0
  10. package/hooks/hooks.json +2 -1
  11. package/hooks/register.mjs +12 -0
  12. package/package.json +4 -1
  13. package/skills/3d-intro-build/SKILL.md +1 -1
  14. package/skills/3d-intro-setup/SKILL.md +1 -1
  15. package/skills/all-in-one/SKILL.md +160 -55
  16. package/skills/append_wiki/SKILL.md +1 -1
  17. package/skills/arch-diagram/SKILL.md +1 -1
  18. package/skills/audit-mock/SKILL.md +1 -1
  19. package/skills/audit-security/SKILL.md +1 -1
  20. package/skills/audit-web-page/SKILL.md +1 -1
  21. package/skills/cleansing-memory/SKILL.md +1 -1
  22. package/skills/compact-copy/SKILL.md +14 -13
  23. package/skills/compact-wiki/SKILL.md +1 -1
  24. package/skills/curation/SKILL.md +36 -3
  25. package/skills/deep-init/SKILL.md +1 -1
  26. package/skills/deep-research/SKILL.md +1 -1
  27. package/skills/docs-setup/SKILL.md +1 -1
  28. package/skills/humanizer/SKILL.md +1 -1
  29. package/skills/interval-report/SKILL.md +1 -1
  30. package/skills/lineage/SKILL.md +146 -16
  31. package/skills/lineage/lineage.py +1091 -107
  32. package/skills/make-notion-guide/SKILL.md +1 -1
  33. package/skills/motion-graphic-make/SKILL.md +1 -1
  34. package/skills/motion-graphic-setup/SKILL.md +1 -1
  35. package/skills/nothing-design/SKILL.md +1 -1
  36. package/skills/obsidizer/SKILL.md +1 -1
  37. package/skills/omc-reference/SKILL.md +68 -33
  38. package/skills/payload-mon/SKILL.md +66 -0
  39. package/skills/payload-mon/scripts/payload-mon.mjs +377 -0
  40. package/skills/payload-mon/scripts/payload-size.mjs +238 -0
  41. package/skills/pdf-vision-extract/SKILL.md +1 -1
  42. package/skills/play-qa/SKILL.md +1 -1
  43. package/skills/ralph-qa/SKILL.md +263 -112
  44. package/skills/ralph-qa/references/gemini-read-only.toml +9 -0
  45. package/skills/ralph-qa/references/gemini-seat.mjs +175 -0
  46. package/skills/ralph-qa/references/payload-scan.mjs +130 -0
  47. package/skills/ralph-qa/references/verifier-probe.mjs +872 -780
  48. package/skills/ready-compact/SKILL.md +114 -67
  49. package/skills/refresh-git-ignore/SKILL.md +1 -1
  50. package/skills/refresh-readme/SKILL.md +3 -3
  51. package/skills/rfp-author/SKILL.md +3 -3
  52. package/skills/setup-bypass-permissions/SKILL.md +123 -0
  53. package/skills/setup-bypass-permissions/agents/openai.yaml +4 -0
  54. package/skills/setup-bypass-permissions/scripts/bypass-permissions.mjs +381 -0
  55. package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.ps1 +247 -0
  56. package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +241 -0
  57. package/skills/{harness-factory → setup-harness-factory}/SKILL.md +3 -3
  58. package/skills/setup-insane-search/SKILL.md +1 -1
  59. package/skills/setup-java/SKILL.md +1 -1
  60. package/skills/setup-lsp/SKILL.md +1 -1
  61. package/skills/setup-mcp/SKILL.md +1 -1
  62. package/skills/setup-node/SKILL.md +1 -1
  63. package/skills/setup-omc/SKILL.md +11 -4
  64. package/skills/setup-omc-hud/SKILL.md +26 -1
  65. package/skills/setup-omc-hud/scripts/claude-update-last.mjs +247 -0
  66. package/skills/setup-playwright/SKILL.md +1 -1
  67. package/skills/setup-pwsh/SKILL.md +1 -1
  68. package/skills/setup-python/SKILL.md +1 -1
  69. package/skills/setup-sandbox/SKILL.md +1 -1
  70. package/skills/setup-stitch/SKILL.md +1 -1
  71. package/skills/setup-tmux/SKILL.md +1 -1
  72. package/skills/smart-compact/SKILL.md +10 -9
  73. package/skills/summary-wiki/SKILL.md +1 -1
  74. package/skills/tone-compact/SKILL.md +203 -0
  75. package/skills/tone-compact/scripts/tone-compact.mjs +499 -0
  76. package/skills/ultra-init/SKILL.md +53 -42
  77. package/skills/ultra-interview/SKILL.md +1 -1
  78. package/skills/ultra-ui-qa/SKILL.md +1 -1
  79. package/skills/update-banker/SKILL.md +1 -1
  80. package/skills/vertical-pptx/SKILL.md +1 -1
  81. package/skills/vertical-pptx-setup/SKILL.md +1 -1
  82. 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'"