claudecode-linter 2.1.148-patch.2 → 2.1.148-patch.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/README.md CHANGED
@@ -23,6 +23,17 @@ Or run directly:
23
23
  npx claudecode-linter ~/projects/my-plugin/
24
24
  ```
25
25
 
26
+ Or build it from a clone of this repository — there is no `dist/` until you build:
27
+
28
+ ```bash
29
+ git clone https://github.com/retif/claudecode-linter
30
+ cd claudecode-linter
31
+ npm ci && npm run build # install dependencies, compile to dist/
32
+ node dist/index.js path/to/plugin/
33
+ ```
34
+
35
+ Commands elsewhere in this README are written as `claudecode-linter …` (the global / `npx` install); from a clone, that is `node dist/index.js …`.
36
+
26
37
  ## Usage
27
38
 
28
39
  ### Lint
@@ -211,6 +222,82 @@ Formatting is powered by [prettier](https://prettier.io/) for consistent JSON an
211
222
  | 1 | Lint errors found |
212
223
  | 2 | Fatal error |
213
224
 
225
+ ## Running on untrusted plugins
226
+
227
+ claudecode-linter is a **static analyzer** — it parses and validates the artifacts it inspects, it never executes them. There is no `eval`, no `child_process`, no declared hooks are run, and no MCP servers are spawned. Linting **trusted** code needs no special isolation.
228
+
229
+ For **untrusted** plugins — especially with `--fix`, which writes files back to disk — run the linter sandboxed. claudecode-linter is verified to run correctly fully confined: no network, a read-only root filesystem, all Linux capabilities dropped, `no-new-privileges`, a non-root UID, and only the target directory mounted.
230
+
231
+ ### The Docker image
232
+
233
+ Two multi-arch (`linux/amd64`, `linux/arm64`) images are published to the GitHub Container Registry — two separate packages, each with its own `:latest` rolling tag and `:<version>` tag:
234
+
235
+ | Image | Built from | Notes |
236
+ |-------|-----------|-------|
237
+ | `ghcr.io/retif/node-claudecode-linter` | `Dockerfile` — `node:24-alpine` | default |
238
+ | `ghcr.io/retif/bun-claudecode-linter` | `Dockerfile.compile` — `bun build --compile` single executable | smaller (~44 MB compressed) |
239
+
240
+ **Pull a published image:**
241
+
242
+ ```bash
243
+ docker pull ghcr.io/retif/node-claudecode-linter # default (node:24-alpine)
244
+ docker pull ghcr.io/retif/bun-claudecode-linter # smaller (bun --compile)
245
+ ```
246
+
247
+ **Or build it locally** from a checkout of this repo:
248
+
249
+ ```bash
250
+ docker build -t node-claudecode-linter . # default (Dockerfile)
251
+ docker build -f Dockerfile.compile -t bun-claudecode-linter . # smaller variant
252
+ ```
253
+
254
+ Both images behave identically. The `docker run` recipes below use `ghcr.io/retif/node-claudecode-linter`; substitute `ghcr.io/retif/bun-claudecode-linter` or a locally-built tag as you prefer.
255
+
256
+ ### Sandboxed invocation
257
+
258
+ **Docker — read-only lint:**
259
+
260
+ ```bash
261
+ docker run --rm --network none --read-only --tmpfs /tmp \
262
+ --user "$(id -u):$(id -g)" --cap-drop ALL --security-opt no-new-privileges \
263
+ -v "$PWD":/work:ro -w /work ghcr.io/retif/node-claudecode-linter /work
264
+ ```
265
+
266
+ **Docker — `--fix`:** the mount must be read-write so fixes can be written back. Otherwise identical, plus the `--fix` flag:
267
+
268
+ ```bash
269
+ docker run --rm --network none --read-only --tmpfs /tmp \
270
+ --user "$(id -u):$(id -g)" --cap-drop ALL --security-opt no-new-privileges \
271
+ -v "$PWD":/work -w /work ghcr.io/retif/node-claudecode-linter --fix /work
272
+ ```
273
+
274
+ All four recipes here are verified. On Linux without Docker, [bubblewrap](https://github.com/containers/bubblewrap) (`bwrap`) gives the equivalent boundary: `--unshare-all` cuts network (confirmed: `ECONNREFUSED` inside the sandbox), and nothing is writable except — for `--fix` — the target directory (confirmed: a write outside it is refused).
275
+
276
+ **bwrap — read-only (lint):**
277
+
278
+ ```bash
279
+ bwrap \
280
+ --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \
281
+ --unshare-all --die-with-parent \
282
+ --chdir "$PWD" \
283
+ claudecode-linter "$PWD"
284
+ ```
285
+
286
+ **bwrap — read-write (`--fix`):** the later `--bind` overrides the read-only root for just the target directory:
287
+
288
+ ```bash
289
+ bwrap \
290
+ --ro-bind / / --bind "$PWD" "$PWD" \
291
+ --dev /dev --proc /proc --tmpfs /tmp \
292
+ --unshare-all --die-with-parent \
293
+ --chdir "$PWD" \
294
+ claudecode-linter --fix "$PWD"
295
+ ```
296
+
297
+ `--ro-bind / /` can be replaced with explicit per-path `--ro-bind` entries (e.g. just `/usr`, `/nix`, and the target) for least-read-authority.
298
+
299
+ See [`SECURITY.md`](SECURITY.md) for the full security model, the audited input-handling hardening, and how to report a vulnerability.
300
+
214
301
  ## Versioning
215
302
 
216
303
  This linter's version tracks the Claude Code version it was extracted from:
package/dist/config.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
2
+ import { join } from "node:path";
4
3
  import { homedir } from "node:os";
5
4
  import { parse as parseYaml } from "yaml";
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ import { assetCandidates } from "./utils/asset-path.js";
7
6
  const DEFAULT_CONFIG = {
8
7
  rules: {},
9
8
  };
@@ -13,7 +12,7 @@ export function loadConfig(configPath) {
13
12
  return DEFAULT_CONFIG;
14
13
  try {
15
14
  const content = readFileSync(path, "utf-8");
16
- const parsed = parseYaml(content);
15
+ const parsed = parseYaml(content, { maxAliasCount: 100 });
17
16
  if (!parsed || typeof parsed !== "object")
18
17
  return DEFAULT_CONFIG;
19
18
  const config = { rules: {} };
@@ -47,10 +46,16 @@ function findConfigFile() {
47
46
  if (existsSync(homePath))
48
47
  return homePath;
49
48
  }
50
- // 3. Fall back to bundled defaults
51
- const bundled = join(__dirname, "..", ".claudecode-lint.defaults.yaml");
52
- if (existsSync(bundled))
53
- return bundled;
49
+ // 3. Fall back to bundled defaults. Resolved relative to import.meta.url
50
+ // first (Node / npm package), then relative to process.execPath as a
51
+ // fallback for the `bun build --compile` single-executable variant.
52
+ for (const bundled of assetCandidates(import.meta.url, [
53
+ "..",
54
+ ".claudecode-lint.defaults.yaml",
55
+ ])) {
56
+ if (existsSync(bundled))
57
+ return bundled;
58
+ }
54
59
  return undefined;
55
60
  }
56
61
  export function mergeCliRules(config, enable, disable) {
@@ -1,4 +1,5 @@
1
1
  import pc from "picocolors";
2
+ import { sanitizeForTerminal } from "../utils/terminal.js";
2
3
  const SEVERITY_ICONS = {
3
4
  error: pc.red("error"),
4
5
  warning: pc.yellow("warn "),
@@ -16,12 +17,12 @@ export function formatHuman(results, quiet) {
16
17
  if (filtered.length === 0)
17
18
  continue;
18
19
  lines.push("");
19
- lines.push(pc.underline(result.file));
20
+ lines.push(pc.underline(sanitizeForTerminal(result.file)));
20
21
  for (const d of filtered) {
21
22
  const loc = d.line
22
23
  ? pc.dim(`:${d.line}${d.column ? `:${d.column}` : ""}`)
23
24
  : "";
24
- lines.push(` ${SEVERITY_ICONS[d.severity]} ${d.message} ${pc.dim(d.rule)}${loc}`);
25
+ lines.push(` ${SEVERITY_ICONS[d.severity]} ${sanitizeForTerminal(d.message)} ${pc.dim(d.rule)}${loc}`);
25
26
  if (d.severity === "error")
26
27
  errorCount++;
27
28
  else if (d.severity === "warning")
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
2
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, statSync, } from "node:fs";
3
3
  import { resolve, relative, dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import sade from "sade";
6
6
  import pc from "picocolors";
7
7
  import { loadConfig, mergeCliRules } from "./config.js";
8
+ import { assetCandidates } from "./utils/asset-path.js";
9
+ import { writeBlockedReason } from "./utils/safe-write.js";
10
+ import { sanitizeForTerminal } from "./utils/terminal.js";
8
11
  import { discoverArtifacts, detectArtifactTypes } from "./discovery.js";
9
12
  import { formatHuman } from "./formatters/human.js";
10
13
  import { formatJson } from "./formatters/json.js";
@@ -71,6 +74,12 @@ const ALL_RULES = [
71
74
  ...MONITORS_JSON_RULES,
72
75
  ...MISPLACED_FILE_RULES,
73
76
  ];
77
+ /**
78
+ * Hard cap on artifact file size. Real Claude Code artifacts are KB-scale;
79
+ * this only exists to reject pathological / malicious inputs (e.g. a
80
+ * multi-gigabyte file crafted to exhaust memory).
81
+ */
82
+ const MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
74
83
  function simpleDiff(oldContent, newContent, filePath) {
75
84
  if (oldContent === newContent)
76
85
  return "";
@@ -106,7 +115,21 @@ function simpleDiff(oldContent, newContent, filePath) {
106
115
  }
107
116
  return lines.join("\n");
108
117
  }
109
- const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
118
+ function readPkgVersion() {
119
+ // package.json ships beside dist/ (Node) or beside the executable
120
+ // (bun-compiled single-executable). Try every candidate; fall back to
121
+ // "0.0.0" if none resolve (e.g. an unexpected layout) rather than crashing.
122
+ for (const p of assetCandidates(import.meta.url, ["..", "package.json"])) {
123
+ try {
124
+ return JSON.parse(readFileSync(p, "utf8")).version;
125
+ }
126
+ catch {
127
+ // try next candidate
128
+ }
129
+ }
130
+ return "0.0.0";
131
+ }
132
+ const pkgVersion = readPkgVersion();
110
133
  sade("claudecode-linter", true)
111
134
  .version(pkgVersion)
112
135
  .describe("Linter for Claude Code plugin artifacts")
@@ -133,9 +156,11 @@ sade("claudecode-linter", true)
133
156
  const fixDryRun = !!opts["fix-dry-run"];
134
157
  try {
135
158
  if (opts.init !== undefined) {
136
- const __filename = fileURLToPath(import.meta.url);
137
- const pkgDir = dirname(dirname(__filename));
138
- const defaultsFile = join(pkgDir, ".claudecode-lint.defaults.yaml");
159
+ const defaultsFile = assetCandidates(import.meta.url, [
160
+ "..",
161
+ ".claudecode-lint.defaults.yaml",
162
+ ]).find((p) => existsSync(p)) ??
163
+ join(dirname(dirname(fileURLToPath(import.meta.url))), ".claudecode-lint.defaults.yaml");
139
164
  const targetDir = typeof opts.init === "string" ? resolve(opts.init) : process.cwd();
140
165
  const targetFile = join(targetDir, ".claudecode-lint.yaml");
141
166
  if (existsSync(targetFile)) {
@@ -197,10 +222,33 @@ sade("claudecode-linter", true)
197
222
  ignore: ignorePatterns,
198
223
  });
199
224
  if (artifacts.length === 0) {
200
- process.stderr.write(pc.yellow(`No plugin artifacts found in ${targetPath}\n`));
225
+ process.stderr.write(pc.yellow(`No plugin artifacts found in ${sanitizeForTerminal(targetPath)}\n`));
201
226
  continue;
202
227
  }
228
+ // All --fix / --format writes for this target must stay inside
229
+ // rootDir. If targetPath is a file, rootDir is its parent dir.
230
+ const resolvedTarget = resolve(targetPath);
231
+ let rootDir = resolvedTarget;
232
+ try {
233
+ if (!statSync(resolvedTarget).isDirectory()) {
234
+ rootDir = dirname(resolvedTarget);
235
+ }
236
+ }
237
+ catch {
238
+ rootDir = dirname(resolvedTarget);
239
+ }
203
240
  for (const artifact of artifacts) {
241
+ let sizeBytes;
242
+ try {
243
+ sizeBytes = statSync(artifact.filePath).size;
244
+ }
245
+ catch {
246
+ sizeBytes = 0;
247
+ }
248
+ if (sizeBytes > MAX_ARTIFACT_BYTES) {
249
+ process.stderr.write(pc.yellow(`Skipping ${sanitizeForTerminal(artifact.filePath)}: file exceeds ${MAX_ARTIFACT_BYTES}-byte limit (${sizeBytes} bytes)\n`));
250
+ continue;
251
+ }
204
252
  let content = readFileSync(artifact.filePath, "utf-8");
205
253
  const relPath = relative(process.cwd(), artifact.filePath);
206
254
  if (opts.format) {
@@ -208,8 +256,14 @@ sade("claudecode-linter", true)
208
256
  if (fixer) {
209
257
  const fixedContent = await fixer.fix(artifact.filePath, content, config);
210
258
  if (fixedContent !== content) {
211
- writeFileSync(artifact.filePath, fixedContent);
212
- formatted.push(relPath);
259
+ const blocked = writeBlockedReason(artifact.filePath, rootDir);
260
+ if (blocked) {
261
+ process.stderr.write(pc.red(`Refusing to write ${sanitizeForTerminal(artifact.filePath)}: ${blocked}\n`));
262
+ }
263
+ else {
264
+ writeFileSync(artifact.filePath, fixedContent);
265
+ formatted.push(relPath);
266
+ }
213
267
  }
214
268
  }
215
269
  continue;
@@ -221,9 +275,15 @@ sade("claudecode-linter", true)
221
275
  if (fixer) {
222
276
  const fixedContent = await fixer.fix(artifact.filePath, content, config);
223
277
  if (fixedContent !== content) {
224
- writeFileSync(artifact.filePath, fixedContent);
225
- content = fixedContent;
226
- fixed = 1;
278
+ const blocked = writeBlockedReason(artifact.filePath, rootDir);
279
+ if (blocked) {
280
+ process.stderr.write(pc.red(`Refusing to write ${sanitizeForTerminal(artifact.filePath)}: ${blocked}\n`));
281
+ }
282
+ else {
283
+ writeFileSync(artifact.filePath, fixedContent);
284
+ content = fixedContent;
285
+ fixed = 1;
286
+ }
227
287
  }
228
288
  }
229
289
  }
@@ -233,7 +293,7 @@ sade("claudecode-linter", true)
233
293
  const fixedContent = await fixer.fix(artifact.filePath, content, config);
234
294
  if (fixedContent !== content) {
235
295
  const diff = simpleDiff(content, fixedContent, artifact.filePath);
236
- process.stdout.write(diff + "\n");
296
+ process.stdout.write(sanitizeForTerminal(diff) + "\n");
237
297
  }
238
298
  }
239
299
  }
@@ -8,11 +8,10 @@
8
8
  * resolver if the file is being run from an alternate layout (e.g. monorepo).
9
9
  */
10
10
  import { readFileSync } from "node:fs";
11
- import { dirname, resolve } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
11
  import { Ajv2020 } from "ajv/dist/2020.js";
14
12
  // biome-ignore lint/style/useImportType: runtime import; types only.
15
13
  import * as addFormatsNs from "ajv-formats";
14
+ import { assetCandidates } from "./utils/asset-path.js";
16
15
  // ajv-formats ships as CJS with a default function export. Under Node16
17
16
  // ESM resolution we have to reach in for `.default`.
18
17
  const addFormats = addFormatsNs.default;
@@ -29,10 +28,9 @@ function getAjv() {
29
28
  function loadCompiledSchema(fileName) {
30
29
  if (compiledCache.has(fileName))
31
30
  return compiledCache.get(fileName) ?? null;
32
- const here = dirname(fileURLToPath(import.meta.url));
33
31
  const candidates = [
34
- resolve(here, "..", "contracts", fileName),
35
- resolve(here, "..", "..", "contracts", fileName),
32
+ ...assetCandidates(import.meta.url, ["..", "contracts", fileName]),
33
+ ...assetCandidates(import.meta.url, ["..", "..", "contracts", fileName]),
36
34
  ];
37
35
  let raw = null;
38
36
  for (const p of candidates) {
@@ -84,10 +82,14 @@ export function loadCommandFrontmatterSchema() {
84
82
  export function loadPluginSchema() {
85
83
  if (cached)
86
84
  return cached;
87
- const here = dirname(fileURLToPath(import.meta.url));
88
85
  const candidates = [
89
- resolve(here, "..", "contracts", "plugin.schema.json"),
90
- resolve(here, "..", "..", "contracts", "plugin.schema.json"),
86
+ ...assetCandidates(import.meta.url, ["..", "contracts", "plugin.schema.json"]),
87
+ ...assetCandidates(import.meta.url, [
88
+ "..",
89
+ "..",
90
+ "contracts",
91
+ "plugin.schema.json",
92
+ ]),
91
93
  ];
92
94
  let raw = null;
93
95
  for (const p of candidates) {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Resolve runtime assets (contracts/*.schema.json, .claudecode-lint.defaults.yaml,
3
+ * package.json) that ship alongside the package on disk.
4
+ *
5
+ * Normally these are found relative to `import.meta.url` — the location of the
6
+ * compiled `.js` file inside `dist/`. That works for the Node build and for the
7
+ * published npm package.
8
+ *
9
+ * Inside a `bun build --compile` single-executable, `import.meta.url` points
10
+ * into Bun's virtual embedded filesystem (`/$bunfs/...`), so disk reads of
11
+ * sibling assets fail. To support that variant, we ALSO emit candidates
12
+ * relative to `process.execPath` (the real on-disk path of the running
13
+ * executable). The compiled-binary image ships `contracts/` and
14
+ * `.claudecode-lint.defaults.yaml` next to the executable, so those fallback
15
+ * candidates resolve there.
16
+ *
17
+ * For the Node runtime, the `process.execPath`-relative candidates simply point
18
+ * at the `node` binary's directory and won't match — harmless extra lookups
19
+ * appended AFTER the existing ones, so Node resolution is byte-for-byte
20
+ * unchanged.
21
+ */
22
+ import { dirname, resolve } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ /**
25
+ * Build a list of candidate paths for an asset shipped with the package.
26
+ *
27
+ * @param importMetaUrl `import.meta.url` of the calling module.
28
+ * @param segments Path segments, relative to a base directory, that
29
+ * locate the asset (e.g. `["..", "contracts", "x.json"]`
30
+ * for a module under `dist/`).
31
+ * @returns Ordered candidate paths: `import.meta.url`-relative first (existing
32
+ * behavior), then `process.execPath`-relative fallbacks.
33
+ */
34
+ export function assetCandidates(importMetaUrl, segments) {
35
+ const candidates = [];
36
+ // 1. import.meta.url-relative — the existing, primary resolution.
37
+ const here = dirname(fileURLToPath(importMetaUrl));
38
+ candidates.push(resolve(here, ...segments));
39
+ // 2. process.execPath-relative fallbacks for the compiled single-executable.
40
+ // The binary lives next to `contracts/` and the defaults YAML, so we try
41
+ // both the executable's own directory and one level up (mirroring the
42
+ // dist/ -> package-root step the segments encode).
43
+ try {
44
+ const execDir = dirname(process.execPath);
45
+ // Drop leading ".." segments: assets sit directly beside the executable.
46
+ const beside = segments.filter((s) => s !== "..");
47
+ candidates.push(resolve(execDir, ...beside));
48
+ candidates.push(resolve(execDir, ...segments));
49
+ }
50
+ catch {
51
+ // process.execPath unavailable — ignore.
52
+ }
53
+ // De-duplicate while preserving order.
54
+ return [...new Set(candidates)];
55
+ }
56
+ //# sourceMappingURL=asset-path.js.map
@@ -32,7 +32,7 @@ export function parseFrontmatter(content) {
32
32
  const body = lines.slice(closingIndex + 1).join("\n");
33
33
  const bodyStartLine = closingIndex + 2; // 1-based
34
34
  try {
35
- const data = parseYaml(frontmatterRaw);
35
+ const data = parseYaml(frontmatterRaw, { maxAliasCount: 100 });
36
36
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
37
37
  return {
38
38
  data: {},
@@ -0,0 +1,36 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import { sep } from "node:path";
3
+ /**
4
+ * Decide whether writing to `filePath` (a fix/format target) is safe.
5
+ *
6
+ * The linter may write fixes back to artifact paths supplied by a plugin.
7
+ * A malicious plugin can ship an artifact path that is actually a symlink
8
+ * pointing outside the target tree (`~/.bashrc`, an SSH key, a CI secret);
9
+ * a bare `writeFileSync` would then clobber the symlink's target.
10
+ *
11
+ * Returns a human-readable reason to REFUSE the write, or `null` if the
12
+ * write is safe. The check fails closed: if anything throws (path missing,
13
+ * permission error, etc.) a refusal reason is returned.
14
+ *
15
+ * Refuses when:
16
+ * - `filePath` itself is a symbolic link, or
17
+ * - the real path of `filePath` is not `rootDir` itself and not located
18
+ * under `rootDir + path.sep`.
19
+ */
20
+ export function writeBlockedReason(filePath, rootDir) {
21
+ try {
22
+ if (lstatSync(filePath).isSymbolicLink()) {
23
+ return "path is a symlink";
24
+ }
25
+ const realRoot = realpathSync(rootDir);
26
+ const realPath = realpathSync(filePath);
27
+ if (realPath !== realRoot && !realPath.startsWith(realRoot + sep)) {
28
+ return "path resolves outside the target directory";
29
+ }
30
+ return null;
31
+ }
32
+ catch {
33
+ return "path could not be safely resolved";
34
+ }
35
+ }
36
+ //# sourceMappingURL=safe-write.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Strip C0 control characters and DEL from a string before it is written to
3
+ * a terminal, while preserving tab and newline.
4
+ *
5
+ * Diagnostic messages, file paths and `--fix-dry-run` diffs embed untrusted
6
+ * strings (rule content, field values, file content, plugin-controlled file
7
+ * and directory names). Without sanitization an attacker-supplied artifact
8
+ * could smuggle ANSI/control sequences into the user's terminal.
9
+ *
10
+ * Strips U+0000-U+0008, U+000B-U+001F and U+007F (DEL) - every C0 control
11
+ * char and DEL except U+0009 (tab) and U+000A (newline), which are kept.
12
+ * The stripped set includes U+000D (CR) and U+001B (ESC) by design.
13
+ */
14
+ export function sanitizeForTerminal(s) {
15
+ // eslint-disable-next-line no-control-regex
16
+ return s.replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, "");
17
+ }
18
+ //# sourceMappingURL=terminal.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudecode-linter",
3
- "version": "2.1.148-patch.2",
3
+ "version": "2.1.148-patch.4",
4
4
  "description": "Standalone linter for Claude Code plugins and configuration files",
5
5
  "type": "module",
6
6
  "bin": {