ccdesk 0.2.2 → 0.2.4
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/CHANGELOG.md +46 -0
- package/README.md +7 -4
- package/dist/core/agent-edit.d.ts +6 -0
- package/dist/{server → core}/agent-edit.js +15 -21
- package/dist/core/backups.d.ts +19 -0
- package/dist/{server → core}/backups.js +64 -48
- package/dist/core/core.d.ts +96 -0
- package/dist/core/core.js +57 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +38 -0
- package/dist/core/mcp-edit.d.ts +23 -0
- package/dist/{server → core}/mcp-edit.js +121 -100
- package/dist/core/mutations.d.ts +20 -0
- package/dist/{server → core}/mutations.js +8 -15
- package/dist/core/open.d.ts +28 -0
- package/dist/{server → core}/open.js +38 -13
- package/dist/core/parse-edit.d.ts +12 -0
- package/dist/core/parse-edit.js +64 -0
- package/dist/core/reference.d.ts +3 -0
- package/dist/core/reference.js +11 -0
- package/dist/core/scan/compare.d.ts +7 -0
- package/dist/{server → core}/scan/compare.js +2 -2
- package/dist/core/scan/context.d.ts +13 -0
- package/dist/{server → core}/scan/context.js +55 -32
- package/dist/core/scan/data.d.ts +14 -0
- package/dist/core/scan/data.js +26 -0
- package/dist/core/scan/env.d.ts +28 -0
- package/dist/{server → core}/scan/env.js +15 -4
- package/dist/core/scan/files.d.ts +68 -0
- package/dist/core/scan/files.js +270 -0
- package/dist/core/scan/git.d.ts +20 -0
- package/dist/core/scan/git.js +106 -0
- package/dist/core/scan/index.d.ts +5 -0
- package/dist/{server → core}/scan/index.js +119 -82
- package/dist/core/scan/instructions.d.ts +54 -0
- package/dist/{server → core}/scan/instructions.js +141 -73
- package/dist/core/scan/inventory.d.ts +38 -0
- package/dist/{server → core}/scan/inventory.js +134 -115
- package/dist/core/scan/mcp.d.ts +28 -0
- package/dist/{server → core}/scan/mcp.js +153 -95
- package/dist/core/scan/memory.d.ts +8 -0
- package/dist/{server → core}/scan/memory.js +8 -8
- package/dist/core/scan/path-test.d.ts +9 -0
- package/dist/core/scan/path-test.js +156 -0
- package/dist/core/scan/permissions.d.ts +31 -0
- package/dist/{server → core}/scan/permissions.js +5 -8
- package/dist/core/scan/plugins.d.ts +39 -0
- package/dist/{server → core}/scan/plugins.js +36 -27
- package/dist/core/scan/projects.d.ts +9 -0
- package/dist/{server → core}/scan/projects.js +7 -5
- package/dist/core/scan/sandbox.d.ts +14 -0
- package/dist/{server → core}/scan/sandbox.js +87 -59
- package/dist/core/scan/settings.d.ts +37 -0
- package/dist/{server → core}/scan/settings.js +98 -93
- package/dist/core/scan/tools.d.ts +10 -0
- package/dist/{server → core}/scan/tools.js +28 -20
- package/dist/core/scan/usage.d.ts +19 -0
- package/dist/{server → core}/scan/usage.js +122 -97
- package/dist/core/schema.d.ts +8 -0
- package/dist/core/schema.js +67 -0
- package/dist/core/secrets.d.ts +45 -0
- package/dist/core/secrets.js +207 -0
- package/dist/core/settings-edit.d.ts +9 -0
- package/dist/{server → core}/settings-edit.js +91 -112
- package/dist/core/watch.d.ts +45 -0
- package/dist/{server → core}/watch.js +54 -17
- package/dist/core/write.d.ts +117 -0
- package/dist/{server → core}/write.js +61 -8
- package/dist/server/args.d.ts +32 -0
- package/dist/server/args.js +106 -0
- package/dist/server/cli.d.ts +2 -0
- package/dist/server/cli.js +58 -83
- package/dist/server/commands.d.ts +8 -0
- package/dist/server/commands.js +39 -0
- package/dist/server/server.d.ts +12 -0
- package/dist/server/server.js +80 -195
- package/dist/server/start.d.ts +13 -0
- package/dist/server/start.js +21 -0
- package/dist/shared/compare.d.ts +25 -0
- package/dist/shared/memory.d.ts +6 -0
- package/dist/shared/memory.js +18 -2
- package/dist/shared/requests.d.ts +104 -0
- package/dist/shared/requests.js +73 -0
- package/dist/shared/scopes.d.ts +10 -0
- package/dist/shared/settings-catalog.json +2199 -0
- package/dist/shared/types.d.ts +535 -0
- package/dist/shared/types.js +11 -0
- package/dist/ui/assets/index-C9qqEI5Z.js +60 -0
- package/dist/ui/index.html +1 -1
- package/dist/view/diff.d.ts +16 -0
- package/dist/view/diff.js +85 -0
- package/dist/view/format.d.ts +17 -0
- package/dist/view/format.js +47 -0
- package/dist/view/index.d.ts +11 -0
- package/dist/view/index.js +13 -0
- package/dist/view/lines.d.ts +9 -0
- package/dist/view/lines.js +12 -0
- package/dist/view/path-test.d.ts +3 -0
- package/dist/view/path-test.js +7 -0
- package/dist/view/sections/catalog.d.ts +9 -0
- package/dist/view/sections/catalog.js +47 -0
- package/dist/view/sections/extensions.d.ts +1 -0
- package/dist/view/sections/extensions.js +48 -0
- package/dist/view/sections/hooks.d.ts +1 -0
- package/dist/view/sections/hooks.js +32 -0
- package/dist/view/sections/index.d.ts +6 -0
- package/dist/view/sections/index.js +27 -0
- package/dist/view/sections/instructions.d.ts +6 -0
- package/dist/view/sections/instructions.js +38 -0
- package/dist/view/sections/mcp.d.ts +3 -0
- package/dist/view/sections/mcp.js +38 -0
- package/dist/view/sections/memory.d.ts +1 -0
- package/dist/view/sections/memory.js +31 -0
- package/dist/view/sections/permissions.d.ts +17 -0
- package/dist/view/sections/permissions.js +87 -0
- package/dist/view/sections/settings.d.ts +1 -0
- package/dist/view/sections/settings.js +33 -0
- package/dist/view/sections/tools.d.ts +6 -0
- package/dist/view/sections/tools.js +57 -0
- package/dist/view/sections/types.d.ts +123 -0
- package/dist/view/sections/types.js +10 -0
- package/dist/view/settings.d.ts +12 -0
- package/dist/view/settings.js +31 -0
- package/dist/view/usage.d.ts +8 -0
- package/dist/view/usage.js +18 -0
- package/package.json +13 -2
- package/dist/server/scan/files.js +0 -480
- package/dist/server/scan/path-test.js +0 -136
- package/dist/server/schema.js +0 -15
- package/dist/server/secrets.js +0 -63
- package/dist/ui/assets/index-Dg9dxngR.js +0 -60
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Plugin, type Problem, type SettingsLayer } from "../../shared/types.ts";
|
|
2
|
+
import { type ScanEnv } from "./env.ts";
|
|
3
|
+
/** A plugin with its manifest, which a scan reads once. */
|
|
4
|
+
export interface LoadedPlugin {
|
|
5
|
+
plugin: Plugin;
|
|
6
|
+
/** `.claude-plugin/plugin.json`, or `{}` when there is none or it does not parse. */
|
|
7
|
+
manifest: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
/** Reads a plugin's manifest; one that is there but does not parse goes into `problems`. */
|
|
10
|
+
export declare function loadPlugin(env: ScanEnv, plugin: Plugin, problems: Problem[]): Promise<LoadedPlugin>;
|
|
11
|
+
/**
|
|
12
|
+
* The prefix of a plugin's skills, commands and agents: the manifest's name, which wins over the
|
|
13
|
+
* name the plugin is enabled under (docs: plugins-reference#name, #metadata-precedence).
|
|
14
|
+
*/
|
|
15
|
+
export declare function pluginNamespace({ plugin, manifest }: LoadedPlugin): string;
|
|
16
|
+
/**
|
|
17
|
+
* Installed plugins that apply in `projectDir`, with whether each is enabled and by which file. An
|
|
18
|
+
* install list or manifest that is there but does not parse goes into `problems`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readPlugins(env: ScanEnv, layers: SettingsLayer[], projectDir: string, gitRoot: string | null, problems: Problem[]): Promise<LoadedPlugin[]>;
|
|
21
|
+
/** The paths a manifest key names (one path or a list), inside the plugin; null when unset. */
|
|
22
|
+
export declare function manifestPaths({ plugin, manifest }: LoadedPlugin, key: string): Promise<string[] | null>;
|
|
23
|
+
/**
|
|
24
|
+
* The value of a manifest key that is a path, an inline object, or a list of either. A file it
|
|
25
|
+
* names that is there but does not parse goes into `problems`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function manifestConfigs(env: ScanEnv, { plugin, manifest }: LoadedPlugin, key: string, problems: Problem[]): Promise<Array<{
|
|
28
|
+
value: unknown;
|
|
29
|
+
path: string;
|
|
30
|
+
}>>;
|
|
31
|
+
type ManifestCommand = {
|
|
32
|
+
name: string;
|
|
33
|
+
path: string;
|
|
34
|
+
description: string;
|
|
35
|
+
text: string | null;
|
|
36
|
+
};
|
|
37
|
+
/** The manifest's `commands` object form: command name to a file or inline content. */
|
|
38
|
+
export declare function manifestCommandMap({ plugin, manifest, }: LoadedPlugin): Promise<ManifestCommand[]>;
|
|
39
|
+
export {};
|
|
@@ -2,28 +2,32 @@ import { join, resolve } from "node:path";
|
|
|
2
2
|
import { HIGHEST_FIRST } from "../../shared/scopes.js";
|
|
3
3
|
import {} from "../../shared/types.js";
|
|
4
4
|
import {} from "./env.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isWithin, readJsonOrReport, resolvedPath, stringField } from "./files.js";
|
|
6
|
+
import { isRecord } from "./data.js";
|
|
6
7
|
function manifestPath(plugin) {
|
|
7
8
|
return join(plugin.installPath, ".claude-plugin", "plugin.json");
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return
|
|
10
|
+
/** Reads a plugin's manifest; one that is there but does not parse goes into `problems`. */
|
|
11
|
+
export async function loadPlugin(env, plugin, problems) {
|
|
12
|
+
return { plugin, manifest: (await readJsonOrReport(env, manifestPath(plugin), problems)) ?? {} };
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* The prefix of a plugin's skills, commands and agents: the manifest's name, which wins over the
|
|
15
16
|
* name the plugin is enabled under (docs: plugins-reference#name, #metadata-precedence).
|
|
16
17
|
*/
|
|
17
|
-
export
|
|
18
|
-
return stringField(
|
|
18
|
+
export function pluginNamespace({ plugin, manifest }) {
|
|
19
|
+
return stringField(manifest, "name") || (plugin.id.split("@")[0] ?? plugin.id);
|
|
19
20
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Installed plugins that apply in `projectDir`, with whether each is enabled and by which file. An
|
|
23
|
+
* install list or manifest that is there but does not parse goes into `problems`.
|
|
24
|
+
*/
|
|
25
|
+
export async function readPlugins(env, layers, projectDir, gitRoot, problems) {
|
|
26
|
+
const installed = await readJsonOrReport(env, join(env.claudeDir, "plugins", "installed_plugins.json"), problems);
|
|
27
|
+
if (!installed || !isRecord(installed.plugins))
|
|
24
28
|
return [];
|
|
25
29
|
const out = [];
|
|
26
|
-
for (const [id, installs] of Object.entries(installed.
|
|
30
|
+
for (const [id, installs] of Object.entries(installed.plugins)) {
|
|
27
31
|
if (!Array.isArray(installs))
|
|
28
32
|
continue;
|
|
29
33
|
// A project or local install applies only in its own project; it beats a user install there.
|
|
@@ -41,6 +45,7 @@ export async function readPlugins(env, layers, projectDir, gitRoot) {
|
|
|
41
45
|
enabled: true,
|
|
42
46
|
note: "",
|
|
43
47
|
};
|
|
48
|
+
const loaded = await loadPlugin(env, plugin, problems);
|
|
44
49
|
const setting = HIGHEST_FIRST.flatMap((scope) => layers
|
|
45
50
|
.filter((l) => l.scope === scope)
|
|
46
51
|
.map((l) => l.data?.enabledPlugins)
|
|
@@ -53,12 +58,12 @@ export async function readPlugins(env, layers, projectDir, gitRoot) {
|
|
|
53
58
|
plugin.note = `${plugin.enabled ? "Enabled" : "Turned off"} in ${setting.scope} settings`;
|
|
54
59
|
}
|
|
55
60
|
else {
|
|
56
|
-
plugin.enabled =
|
|
61
|
+
plugin.enabled = loaded.manifest.defaultEnabled !== false;
|
|
57
62
|
plugin.note = plugin.enabled
|
|
58
63
|
? "On by default (no enabledPlugins entry)"
|
|
59
64
|
: "Off by default (no enabledPlugins entry)";
|
|
60
65
|
}
|
|
61
|
-
out.push(
|
|
66
|
+
out.push(loaded);
|
|
62
67
|
}
|
|
63
68
|
return out;
|
|
64
69
|
}
|
|
@@ -69,13 +74,12 @@ async function inside(plugin, path) {
|
|
|
69
74
|
return isWithin(root, await resolvedPath(target)) ? target : null;
|
|
70
75
|
}
|
|
71
76
|
/** The paths a manifest key names (one path or a list), inside the plugin; null when unset. */
|
|
72
|
-
export async function manifestPaths(
|
|
73
|
-
const value =
|
|
74
|
-
|
|
75
|
-
if (!list)
|
|
77
|
+
export async function manifestPaths({ plugin, manifest }, key) {
|
|
78
|
+
const value = manifest[key];
|
|
79
|
+
if (typeof value !== "string" && !Array.isArray(value))
|
|
76
80
|
return null;
|
|
77
81
|
const out = [];
|
|
78
|
-
for (const item of
|
|
82
|
+
for (const item of typeof value === "string" ? [value] : value) {
|
|
79
83
|
if (typeof item !== "string")
|
|
80
84
|
continue;
|
|
81
85
|
const path = await inside(plugin, item);
|
|
@@ -84,25 +88,30 @@ export async function manifestPaths(env, plugin, key) {
|
|
|
84
88
|
}
|
|
85
89
|
return out;
|
|
86
90
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The value of a manifest key that is a path, an inline object, or a list of either. A file it
|
|
93
|
+
* names that is there but does not parse goes into `problems`.
|
|
94
|
+
*/
|
|
95
|
+
export async function manifestConfigs(env, { plugin, manifest }, key, problems) {
|
|
96
|
+
const value = manifest[key];
|
|
97
|
+
if (value === undefined)
|
|
98
|
+
return [];
|
|
90
99
|
const out = [];
|
|
91
|
-
for (const item of Array.isArray(value) ? value :
|
|
100
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
92
101
|
if (isRecord(item))
|
|
93
102
|
out.push({ value: item, path: manifestPath(plugin) });
|
|
94
103
|
if (typeof item !== "string")
|
|
95
104
|
continue;
|
|
96
105
|
const path = await inside(plugin, item);
|
|
97
|
-
const file = path ? await
|
|
98
|
-
if (path && file
|
|
99
|
-
out.push({ value: file
|
|
106
|
+
const file = path ? await readJsonOrReport(env, path, problems) : null;
|
|
107
|
+
if (path && file)
|
|
108
|
+
out.push({ value: file[key] ?? file, path });
|
|
100
109
|
}
|
|
101
110
|
return out;
|
|
102
111
|
}
|
|
103
112
|
/** The manifest's `commands` object form: command name to a file or inline content. */
|
|
104
|
-
export async function manifestCommandMap(
|
|
105
|
-
const value =
|
|
113
|
+
export async function manifestCommandMap({ plugin, manifest, }) {
|
|
114
|
+
const value = manifest.commands;
|
|
106
115
|
if (!isRecord(value))
|
|
107
116
|
return [];
|
|
108
117
|
const out = [];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type MemoryStore, type ProjectSummary } from "../../shared/types.ts";
|
|
2
|
+
import { type ScanEnv } from "./env.ts";
|
|
3
|
+
interface KnownProject extends ProjectSummary {
|
|
4
|
+
/** The project's auto memory, read once for both the counts and the search catalog. */
|
|
5
|
+
store: MemoryStore | null;
|
|
6
|
+
}
|
|
7
|
+
/** Directories Claude Code has been used in: its project stores, `~/.claude.json`, and the launch directory. */
|
|
8
|
+
export declare function listProjects(env: ScanEnv, claudeJson: Record<string, unknown> | null, launchDir: string): Promise<KnownProject[]>;
|
|
9
|
+
export {};
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
1
|
import { stat } from "node:fs/promises";
|
|
3
2
|
import { join, resolve } from "node:path";
|
|
4
3
|
import { createInterface } from "node:readline";
|
|
5
4
|
import { storeDirName } from "../../shared/memory.js";
|
|
6
5
|
import {} from "../../shared/types.js";
|
|
7
6
|
import {} from "./env.js";
|
|
8
|
-
import {
|
|
7
|
+
import { isDir, listDir, openUnlessCredentials } from "./files.js";
|
|
8
|
+
import { isRecord } from "./data.js";
|
|
9
9
|
import { readMemoryStore } from "./memory.js";
|
|
10
10
|
/** Directories Claude Code has been used in: its project stores, `~/.claude.json`, and the launch directory. */
|
|
11
11
|
export async function listProjects(env, claudeJson, launchDir) {
|
|
12
12
|
const byPath = new Map();
|
|
13
13
|
const storesRoot = join(env.claudeDir, "projects");
|
|
14
|
-
|
|
14
|
+
// Claude Code keys Windows folders with forward slashes; listed here as Windows writes them.
|
|
15
|
+
const knownPaths = (isRecord(claudeJson?.projects) ? Object.keys(claudeJson.projects) : []).map((key) => (env.platform === "win32" ? key.replaceAll("/", "\\") : key));
|
|
15
16
|
for (const name of await listDir(storesRoot)) {
|
|
16
17
|
const storeDir = join(storesRoot, name);
|
|
17
18
|
if (!(await isDir(storeDir)))
|
|
@@ -73,9 +74,10 @@ async function newestFirst(dir, files) {
|
|
|
73
74
|
/** The `cwd` recorded in the newest transcripts, for store names that do not map back cleanly. */
|
|
74
75
|
async function sessionCwd(env, storeDir, sessions) {
|
|
75
76
|
for (const { file } of sessions.slice(0, 3)) {
|
|
76
|
-
|
|
77
|
+
const handle = await openUnlessCredentials(env, join(storeDir, file)).catch(() => null);
|
|
78
|
+
if (!handle)
|
|
77
79
|
continue;
|
|
78
|
-
const input = createReadStream(
|
|
80
|
+
const input = handle.createReadStream({ encoding: "utf8" });
|
|
79
81
|
const lines = createInterface({ input });
|
|
80
82
|
let read = 0;
|
|
81
83
|
try {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Scope } from "../../shared/types.ts";
|
|
2
|
+
/** A part of one file's `sandbox` value that Claude Code ignores, and why. */
|
|
3
|
+
export interface Dropped {
|
|
4
|
+
value: unknown;
|
|
5
|
+
reason: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A filter for each file's `sandbox` value: what is left once the fields Claude Code ignores from
|
|
9
|
+
* that file are taken out, and what was taken out.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sandboxFilter(valueIn: (key: string, scope: Scope) => unknown): (scope: Scope, value: unknown) => {
|
|
12
|
+
value: unknown;
|
|
13
|
+
dropped: Dropped[];
|
|
14
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {} from "../../shared/types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { stringsIn } from "./files.js";
|
|
3
|
+
import { isRecord } from "./data.js";
|
|
3
4
|
const REPOSITORY = ["project", "local"];
|
|
4
5
|
const NOT_MANAGED = ["user", "project", "local"];
|
|
5
6
|
function at(value, path) {
|
|
@@ -35,13 +36,30 @@ function replaced(value, path, leaf) {
|
|
|
35
36
|
*/
|
|
36
37
|
function rulesFor(valueIn) {
|
|
37
38
|
const managed = valueIn("sandbox", "managed");
|
|
38
|
-
const user = valueIn("sandbox", "user");
|
|
39
|
-
const managedSays = (path, value) => at(managed, path.split(".")) === value;
|
|
40
39
|
const rules = [];
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const managedFiles = at(managed, ["credentials", "files"]);
|
|
41
|
+
const lock = {
|
|
42
|
+
managed,
|
|
43
|
+
user: valueIn("sandbox", "user"),
|
|
44
|
+
valueIn,
|
|
45
|
+
managedSays: (path, value) => at(managed, path.split(".")) === value,
|
|
46
|
+
// docs: sandboxing#which-settings-can-disable-it
|
|
47
|
+
filesystemLocked: isRecord(at(managed, ["filesystem"])) ||
|
|
48
|
+
(Array.isArray(managedFiles) && managedFiles.some((f) => isRecord(f) && f.mode === "deny")),
|
|
49
|
+
add: (fields, from, reason, drops) => {
|
|
50
|
+
for (const field of fields)
|
|
51
|
+
rules.push({ field, from, reason, ...(drops ? { drops } : {}) });
|
|
52
|
+
},
|
|
44
53
|
};
|
|
54
|
+
addScopeRules(lock);
|
|
55
|
+
addManagedLocks(lock);
|
|
56
|
+
addAdminRequiredRules(lock);
|
|
57
|
+
addLocksWithoutAdmin(lock);
|
|
58
|
+
addReadBlockRules(lock);
|
|
59
|
+
return rules;
|
|
60
|
+
}
|
|
61
|
+
/** Fields read from fewer files than the others, and entries dropped from repository files. */
|
|
62
|
+
function addScopeRules({ add, filesystemLocked }) {
|
|
45
63
|
// Keys read from fewer files (docs: settings-reference, each key's Scope).
|
|
46
64
|
for (const field of [
|
|
47
65
|
"filesystem.allowManagedReadPathsOnly",
|
|
@@ -60,14 +78,13 @@ function rulesFor(valueIn) {
|
|
|
60
78
|
"network.tlsTerminate",
|
|
61
79
|
])
|
|
62
80
|
add([field], REPOSITORY, `Claude Code reads sandbox.${field} only from user or managed settings`);
|
|
63
|
-
// docs: sandboxing#which-settings-can-disable-it
|
|
64
|
-
const managedFiles = at(managed, ["credentials", "files"]);
|
|
65
|
-
const filesystemLocked = isRecord(at(managed, ["filesystem"])) ||
|
|
66
|
-
(Array.isArray(managedFiles) && managedFiles.some((f) => isRecord(f) && f.mode === "deny"));
|
|
67
81
|
add(["filesystem.disabled"], filesystemLocked ? NOT_MANAGED : REPOSITORY, filesystemLocked
|
|
68
82
|
? "Managed settings configure the sandbox filesystem, so only they can turn it off"
|
|
69
83
|
: "Claude Code reads sandbox.filesystem.disabled only from user or managed settings");
|
|
70
84
|
add(["credentials.files", "credentials.envVars"], REPOSITORY, "Claude Code drops mask entries from repository settings", { entries: (e) => isRecord(e) && e.mode === "mask" });
|
|
85
|
+
}
|
|
86
|
+
/** A false allowUnsandboxedCommands that holds, and the managed-only allowlists. */
|
|
87
|
+
function addManagedLocks({ add, user, managedSays }) {
|
|
71
88
|
// docs: sandboxing#turn-off-the-retry-with-strict-sandbox-mode
|
|
72
89
|
if (managedSays("allowUnsandboxedCommands", false) ||
|
|
73
90
|
at(user, ["allowUnsandboxedCommands"]) === false)
|
|
@@ -75,33 +92,37 @@ function rulesFor(valueIn) {
|
|
|
75
92
|
// Managed-only locks (docs: sandboxing#keep-developers-from-widening-the-policy).
|
|
76
93
|
if (managedSays("filesystem.allowManagedReadPathsOnly", true))
|
|
77
94
|
add(["filesystem.allowRead"], NOT_MANAGED, "Managed settings honor only their own allowRead entries (allowManagedReadPathsOnly)");
|
|
78
|
-
|
|
79
|
-
if (domainsLocked)
|
|
95
|
+
if (managedSays("network.allowManagedDomainsOnly", true))
|
|
80
96
|
add(["network.allowedDomains", "network.httpProxyPort", "network.socksProxyPort"], NOT_MANAGED, "Managed settings honor only their own domains and proxy ports (allowManagedDomainsOnly)");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
97
|
+
}
|
|
98
|
+
/** docs: sandboxing#repository-settings-under-an-admin-required-sandbox */
|
|
99
|
+
function addAdminRequiredRules({ add, user, managedSays }) {
|
|
100
|
+
if (!managedSays("allowUnsandboxedCommands", false) &&
|
|
101
|
+
!managedSays("network.allowManagedDomainsOnly", true))
|
|
102
|
+
return;
|
|
103
|
+
const reason = "Managed settings require the sandbox, so Claude Code ignores repository settings that loosen it";
|
|
104
|
+
add([
|
|
105
|
+
"excludedCommands",
|
|
106
|
+
"ignoreViolations",
|
|
107
|
+
"network.allowedDomains",
|
|
108
|
+
"network.allowUnixSockets",
|
|
109
|
+
"network.allowMachLookup",
|
|
110
|
+
"network.httpProxyPort",
|
|
111
|
+
"network.socksProxyPort",
|
|
112
|
+
"filesystem.allowWrite",
|
|
113
|
+
], REPOSITORY, reason);
|
|
114
|
+
add([
|
|
115
|
+
"enableWeakerNestedSandbox",
|
|
116
|
+
"enableWeakerNetworkIsolation",
|
|
117
|
+
"network.allowAllUnixSockets",
|
|
118
|
+
"network.allowLocalBinding",
|
|
119
|
+
], REPOSITORY, reason, { value: true });
|
|
120
|
+
for (const field of ["enabled", "failIfUnavailable"])
|
|
121
|
+
if (at(user, [field]) === true)
|
|
122
|
+
add([field], REPOSITORY, reason, { value: false });
|
|
123
|
+
}
|
|
124
|
+
/** docs: sandboxing#locks-that-apply-without-an-admin-required-sandbox */
|
|
125
|
+
function addLocksWithoutAdmin({ add, managed, user, valueIn }) {
|
|
105
126
|
const managedDeny = stringsIn(at(valueIn("permissions", "managed"), ["deny"]));
|
|
106
127
|
if (stringsIn(at(managed, ["network", "deniedDomains"])).length > 0 ||
|
|
107
128
|
managedDeny.some((rule) => rule.startsWith("WebFetch(domain:")))
|
|
@@ -109,16 +130,38 @@ function rulesFor(valueIn) {
|
|
|
109
130
|
const strictAllowlist = at(managed, ["network", "strictAllowlist"]) ?? at(user, ["network", "strictAllowlist"]);
|
|
110
131
|
if (strictAllowlist === true)
|
|
111
132
|
add(["network.allowedDomains", "network.httpProxyPort", "network.socksProxyPort"], REPOSITORY, "strictAllowlist is on, so Claude Code ignores a repository's domains and proxy ports");
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* While the block covers sandboxed commands, repository allowRead and allowWrite entries do not
|
|
136
|
+
* count; it does not when filesystem isolation is off or allowManagedReadPathsOnly is set
|
|
137
|
+
* (docs: settings-reference#sandboxed-commands-under-the-block).
|
|
138
|
+
*/
|
|
139
|
+
function addReadBlockRules({ add, managed, user, valueIn, managedSays, filesystemLocked, }) {
|
|
115
140
|
const blocked = ["user", "project", "local", "managed"].some((s) => at(valueIn("permissions", s), ["blockReadsOutsideWorkingDirectories"]) === true);
|
|
116
141
|
const filesystemOff = (filesystemLocked ? [managed] : [managed, user])
|
|
117
142
|
.map((v) => at(v, ["filesystem", "disabled"]))
|
|
118
143
|
.find((v) => v !== undefined) === true;
|
|
119
144
|
if (blocked && !filesystemOff && !managedSays("filesystem.allowManagedReadPathsOnly", true))
|
|
120
145
|
add(["filesystem.allowRead", "filesystem.allowWrite"], REPOSITORY, "blockReadsOutsideWorkingDirectories is on, so repository allowRead and allowWrite entries do not count");
|
|
121
|
-
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* What a rule takes out of a field's value and what it leaves, undefined for nothing; null when the
|
|
149
|
+
* rule does not apply to it.
|
|
150
|
+
*/
|
|
151
|
+
function splitField(rule, field) {
|
|
152
|
+
if (field === undefined)
|
|
153
|
+
return null;
|
|
154
|
+
const { drops } = rule;
|
|
155
|
+
if (!drops)
|
|
156
|
+
return { removed: field, kept: undefined };
|
|
157
|
+
if ("value" in drops)
|
|
158
|
+
return field === drops.value ? { removed: field, kept: undefined } : null;
|
|
159
|
+
if (!Array.isArray(field))
|
|
160
|
+
return null;
|
|
161
|
+
const entries = field.filter(drops.entries);
|
|
162
|
+
if (entries.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
return { removed: entries, kept: field.filter((e) => !drops.entries(e)) };
|
|
122
165
|
}
|
|
123
166
|
/**
|
|
124
167
|
* A filter for each file's `sandbox` value: what is left once the fields Claude Code ignores from
|
|
@@ -135,26 +178,11 @@ export function sandboxFilter(valueIn) {
|
|
|
135
178
|
if (!rule.from.includes(scope))
|
|
136
179
|
continue;
|
|
137
180
|
const path = rule.field.split(".");
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
181
|
+
const split = splitField(rule, at(out, path));
|
|
182
|
+
if (!split)
|
|
140
183
|
continue;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
let kept;
|
|
144
|
-
if (drops && "value" in drops) {
|
|
145
|
-
if (field !== drops.value)
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
else if (drops) {
|
|
149
|
-
if (!Array.isArray(field))
|
|
150
|
-
continue;
|
|
151
|
-
removed = field.filter(drops.entries);
|
|
152
|
-
if (removed.length === 0)
|
|
153
|
-
continue;
|
|
154
|
-
kept = field.filter((e) => !drops.entries(e));
|
|
155
|
-
}
|
|
156
|
-
out = replaced(out, path, kept);
|
|
157
|
-
dropped.push({ value: nested(path, removed), reason: rule.reason });
|
|
184
|
+
out = replaced(out, path, split.kept);
|
|
185
|
+
dropped.push({ value: nested(path, split.removed), reason: rule.reason });
|
|
158
186
|
}
|
|
159
187
|
return { value: out, dropped };
|
|
160
188
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type EffectiveSetting, type Scope, type SettingsLayer } from "../../shared/types.ts";
|
|
2
|
+
import { type ScanEnv } from "./env.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Why Claude Code ignores a `permissions.defaultMode` from this file, or undefined when it may apply:
|
|
5
|
+
* a repository's settings cannot start sessions in auto or bypass mode (docs: permission-modes).
|
|
6
|
+
*/
|
|
7
|
+
export declare function ignoredModeReason(scope: Scope, mode: unknown): string | undefined;
|
|
8
|
+
export interface SettingsFile {
|
|
9
|
+
scope: Scope;
|
|
10
|
+
path: string;
|
|
11
|
+
legacy?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** The settings files Claude Code reads in `dir`, highest precedence first. */
|
|
14
|
+
export declare function settingsFiles(env: ScanEnv, dir: string): Promise<SettingsFile[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Settings files that apply in `dir`, highest precedence first. `managed` is what
|
|
17
|
+
* readManagedSettings read, so a scan reads the managed files once.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readSettingsLayers(env: ScanEnv, dir: string, managed: ManagedSettings): Promise<SettingsLayer[]>;
|
|
20
|
+
export interface ManagedSettings {
|
|
21
|
+
data: Record<string, unknown> | null;
|
|
22
|
+
sources: string[];
|
|
23
|
+
dropIns: string[];
|
|
24
|
+
/** Managed sources ccdesk does not read that rank above the files (higherSources). */
|
|
25
|
+
higher: string[];
|
|
26
|
+
errors: string[];
|
|
27
|
+
/** A managed file that is there but not a JSON object: Claude Code refuses to start (docs: managed-settings#find-entries-claude-code-dropped). */
|
|
28
|
+
blocksStart: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* managed-settings.json, then every *.json in managed-settings.d in alphabetical order, merged, with
|
|
32
|
+
* nothing hidden: for checks such as managedMcpServers that must see header values. Never sent to the
|
|
33
|
+
* page as is.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readManagedSettings(env: ScanEnv): Promise<ManagedSettings>;
|
|
36
|
+
/** The value Claude Code ends up with for each top-level key, and where it came from. */
|
|
37
|
+
export declare function effectiveSettings(layers: SettingsLayer[]): EffectiveSetting[];
|