@levnikolaevich/hex-line-mcp 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,8 @@ Hash-verified file editing MCP + token efficiency hook for AI coding agents.
9
9
 
10
10
  Every line carries an FNV-1a content hash. Every edit must present those hashes back -- proving the agent is editing what it thinks it's editing. No stale context, no silent corruption. Hashing works on normalized logical text; writes preserve the file's existing line endings and trailing-newline shape.
11
11
 
12
+ By default, mutating tools stay inside the current project root. If you intentionally need to edit a temp or external path, pass `allow_external: true` on `edit_file`, `write_file`, or `bulk_replace`.
13
+
12
14
  ## Features
13
15
 
14
16
  ### 9 MCP Tools
@@ -182,6 +184,7 @@ Edit using revision-aware hash-verified anchors. Prefer one batched call per fil
182
184
  | `restore_indent` | boolean | no | Auto-fix indentation to match anchor context (default: false) |
183
185
  | `base_revision` | string | no | Prior revision from `read_file` / `edit_file` for same-file follow-up edits |
184
186
  | `conflict_policy` | enum | no | `conservative` or `strict` (default: `conservative`) |
187
+ | `allow_external` | boolean | no | Allow editing a path outside the current project root |
185
188
 
186
189
  Edit operations (JSON array):
187
190
 
@@ -227,6 +230,7 @@ Create a new file or overwrite an existing one. Creates parent directories autom
227
230
  |-----------|------|----------|-------------|
228
231
  | `path` | string | yes | File path |
229
232
  | `content` | string | yes | File content |
233
+ | `allow_external` | boolean | no | Allow writing a path outside the current project root |
230
234
 
231
235
  ### grep_search
232
236
 
@@ -247,7 +251,7 @@ Search file contents using ripgrep. Three output modes: `content` (canonical `se
247
251
  | `context_before` | number | no | Context lines BEFORE match (`-B`) |
248
252
  | `context_after` | number | no | Context lines AFTER match (`-A`) |
249
253
  | `limit` | number | no | Max matches per file (default: 100) |
250
- | `total_limit` | number | no | Total match events across all files; multiline matches count as 1 (0 = unlimited) |
254
+ | `total_limit` | number | no | Total match events across all files; multiline matches count as 1 (default: 200 for `content`, 1000 for `files`/`count`, 0 = unlimited) |
251
255
  | `plain` | boolean | no | Omit hash tags inside block entries, return `lineNum\|content` |
252
256
 
253
257
  `content` mode returns canonical `search_hunk` blocks with per-hunk checksums enabling direct `replace_lines` from grep results without intermediate `read_file`.
package/dist/hook.mjs CHANGED
@@ -439,7 +439,7 @@ function handleSessionStart() {
439
439
  } catch {
440
440
  }
441
441
  }
442
- const msg = styleActive ? "Hex-line MCP available. Output style active.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <note>Follow the active hex-line output style for primary tool choices.</note>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>" : "Hex-line MCP available.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <exploration>\n <rule>Use outline for structure (code + markdown), not Read. ~10-20 lines vs hundreds.</rule>\n <rule>Use read_file with offset/limit or ranges for targeted reads.</rule>\n <rule>Use grep_search before editing to get hash anchors.</rule>\n </exploration>\n <editing>\n <path name='surgical'>grep_search \u2192 edit_file (fastest: hash-verified, no full read needed)</path>\n <path name='exploratory'>outline \u2192 read_file (ranges) \u2192 edit_file with base_revision</path>\n <path name='multi-file'>bulk_replace(path=&quot;&lt;project root&gt;&quot;) for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Auto-fill path from the active file or project root. Do not leave repo scope implicit.</tip>\n <tip>Never invent range_checksum. Copy it from fresh read_file or grep_search blocks.</tip>\n <tip>Prefer set_line or insert_after for small local changes and replace_between for larger bounded rewrites.</tip>\n <tip>Carry revision from read_file into base_revision on edit_file.</tip>\n <tip>If edit returns CONFLICT, call verify \u2014 only reread when STALE.</tip>\n <tip>Avoid large first-pass edit batches. Start with 1-2 hunks, then continue from the returned revision.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n </tips>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>";
442
+ const msg = styleActive ? "Hex-line MCP available. Output style active.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <note>Follow the active hex-line output style for primary tool choices.</note>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>" : "Hex-line MCP available.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <exploration>\n <rule>Use outline for structure (code + markdown), not Read. ~10-20 lines vs hundreds.</rule>\n <rule>Use read_file with offset/limit or ranges for targeted reads.</rule>\n <rule>Use grep_search before editing to get hash anchors.</rule>\n </exploration>\n <editing>\n <path name='surgical'>grep_search \u2192 edit_file (fastest: hash-verified, no full read needed)</path>\n <path name='exploratory'>outline \u2192 read_file (ranges) \u2192 edit_file with base_revision</path>\n <path name='multi-file'>bulk_replace(path=&quot;&lt;project root&gt;&quot;) for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Auto-fill path from the active file or project root. Read-only tools may inspect explicit temp-file paths outside the repo. Mutating tools stay project-scoped unless you intentionally pass allow_external=true.</tip>\n <tip>Never invent range_checksum. Copy it from fresh read_file or grep_search blocks.</tip>\n <tip>Prefer set_line or insert_after for small local changes and replace_between for larger bounded rewrites.</tip>\n <tip>Carry revision from read_file into base_revision on edit_file.</tip>\n <tip>If edit returns CONFLICT, call verify \u2014 only reread when STALE.</tip>\n <tip>Avoid large first-pass edit batches. Start with 1-2 hunks, then continue from the returned revision.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n </tips>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, Glob, .claude/settings.json, and .claude/settings.local.json.</exceptions>\n</hex-line_instructions>";
443
443
  safeExit(1, JSON.stringify({ systemMessage: msg }), 0);
444
444
  }
445
445
  var _norm = (p) => p.replace(/\\/g, "/");
package/dist/server.mjs CHANGED
@@ -107,6 +107,7 @@ import { statSync as statSync4 } from "node:fs";
107
107
 
108
108
  // lib/security.mjs
109
109
  import { realpathSync, statSync as statSync2, existsSync, openSync, readSync, closeSync } from "node:fs";
110
+ import { tmpdir as tmpdir2 } from "node:os";
110
111
  import { resolve, isAbsolute, dirname } from "node:path";
111
112
 
112
113
  // lib/format.mjs
@@ -202,11 +203,43 @@ function readText(filePath) {
202
203
  // lib/security.mjs
203
204
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
204
205
  function normalizePath(p) {
205
- if (process.platform === "win32" && /^\/[a-zA-Z]\//.test(p)) {
206
- p = p[1] + ":" + p.slice(2);
206
+ if (process.platform === "win32") {
207
+ if (p === "/tmp" || p.startsWith("/tmp/")) {
208
+ const suffix = p.slice("/tmp".length).replace(/^\/+/, "");
209
+ p = suffix ? resolve(tmpdir2(), suffix) : tmpdir2();
210
+ } else if (p === "/var/tmp" || p.startsWith("/var/tmp/")) {
211
+ const suffix = p.slice("/var/tmp".length).replace(/^\/+/, "");
212
+ p = suffix ? resolve(tmpdir2(), suffix) : tmpdir2();
213
+ } else if (/^\/[a-zA-Z]\//.test(p)) {
214
+ p = p[1] + ":" + p.slice(2);
215
+ }
207
216
  }
208
217
  return p.replace(/\\/g, "/");
209
218
  }
219
+ function normalizeScopeValue(value) {
220
+ const normalized = value.replace(/\\/g, "/");
221
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
222
+ }
223
+ function resolveInputPath(filePath) {
224
+ const normalized = normalizePath(filePath);
225
+ const abs = isAbsolute(normalized) ? normalized : resolve(process.cwd(), normalized);
226
+ return abs.replace(/\\/g, "/");
227
+ }
228
+ function isWithinRoot(rootPath, targetPath) {
229
+ const root = normalizeScopeValue(rootPath).replace(/\/+$/, "");
230
+ const target = normalizeScopeValue(targetPath);
231
+ return target === root || target.startsWith(`${root}/`);
232
+ }
233
+ function assertProjectScopedPath(filePath, { allowExternal = false } = {}) {
234
+ if (!filePath) throw new Error("Empty file path");
235
+ const abs = resolveInputPath(filePath);
236
+ if (allowExternal) return abs;
237
+ const projectRoot = resolve(process.cwd()).replace(/\\/g, "/");
238
+ if (isWithinRoot(projectRoot, abs)) return abs;
239
+ throw new Error(
240
+ `PATH_OUTSIDE_PROJECT: ${abs}. Editing is restricted to the current project by default. If you intentionally need a temp or external path, retry with allow_external=true.`
241
+ );
242
+ }
210
243
  function validatePath(filePath) {
211
244
  if (!filePath) throw new Error("Empty file path");
212
245
  const normalized = normalizePath(filePath);
@@ -2610,6 +2643,8 @@ try {
2610
2643
  } catch {
2611
2644
  }
2612
2645
  var DEFAULT_LIMIT2 = 100;
2646
+ var DEFAULT_TOTAL_LIMIT_CONTENT = 200;
2647
+ var DEFAULT_TOTAL_LIMIT_LIST = 1e3;
2613
2648
  var MAX_OUTPUT = 10 * 1024 * 1024;
2614
2649
  var TIMEOUT2 = 3e4;
2615
2650
  var MAX_SEARCH_OUTPUT_CHARS = 8e4;
@@ -2651,12 +2686,19 @@ function grepSearch(pattern, opts = {}) {
2651
2686
  const target = normPath ? resolve2(normPath) : process.cwd();
2652
2687
  const output = opts.output || "content";
2653
2688
  const plain = !!opts.plain;
2654
- const totalLimit = opts.totalLimit && opts.totalLimit > 0 ? opts.totalLimit : 0;
2655
- if (output === "files") return filesMode(pattern, target, opts);
2656
- if (output === "count") return countMode(pattern, target, opts);
2689
+ const defaultTotalLimit = output === "content" ? DEFAULT_TOTAL_LIMIT_CONTENT : DEFAULT_TOTAL_LIMIT_LIST;
2690
+ const totalLimit = opts.totalLimit === 0 ? 0 : opts.totalLimit && opts.totalLimit > 0 ? opts.totalLimit : defaultTotalLimit;
2691
+ if (output === "files") return filesMode(pattern, target, opts, totalLimit);
2692
+ if (output === "count") return countMode(pattern, target, opts, totalLimit);
2657
2693
  return contentMode(pattern, target, opts, plain, totalLimit);
2658
2694
  }
2659
- async function filesMode(pattern, target, opts) {
2695
+ function applyListModeTotalLimit(lines, totalLimit) {
2696
+ if (!totalLimit || totalLimit <= 0 || lines.length <= totalLimit) return lines.join("\n");
2697
+ const visible = lines.slice(0, totalLimit);
2698
+ visible.push(`OUTPUT_CAPPED: ${lines.length - totalLimit} more result line(s) omitted. Narrow with path= or glob=, or raise total_limit.`);
2699
+ return visible.join("\n");
2700
+ }
2701
+ async function filesMode(pattern, target, opts, totalLimit) {
2660
2702
  const realArgs = ["-l"];
2661
2703
  if (opts.caseInsensitive) realArgs.push("-i");
2662
2704
  else if (opts.smartCase) realArgs.push("-S");
@@ -2671,9 +2713,9 @@ async function filesMode(pattern, target, opts) {
2671
2713
  if (code !== 0 && code !== null) throw new Error(`GREP_ERROR: rg exit ${code} \u2014 ${stderr.trim() || "unknown error"}`);
2672
2714
  const lines = stdout.trimEnd().split("\n").filter(Boolean);
2673
2715
  const normalized = lines.map((l) => l.replace(/\\/g, "/"));
2674
- return normalized.join("\n");
2716
+ return applyListModeTotalLimit(normalized, totalLimit);
2675
2717
  }
2676
- async function countMode(pattern, target, opts) {
2718
+ async function countMode(pattern, target, opts, totalLimit) {
2677
2719
  const realArgs = ["-c"];
2678
2720
  if (opts.caseInsensitive) realArgs.push("-i");
2679
2721
  else if (opts.smartCase) realArgs.push("-S");
@@ -2688,7 +2730,7 @@ async function countMode(pattern, target, opts) {
2688
2730
  if (code !== 0 && code !== null) throw new Error(`GREP_ERROR: rg exit ${code} \u2014 ${stderr.trim() || "unknown error"}`);
2689
2731
  const lines = stdout.trimEnd().split("\n").filter(Boolean);
2690
2732
  const normalized = lines.map((l) => l.replace(/\\/g, "/"));
2691
- return normalized.join("\n");
2733
+ return applyListModeTotalLimit(normalized, totalLimit);
2692
2734
  }
2693
2735
  async function contentMode(pattern, target, opts, plain, totalLimit) {
2694
2736
  const realArgs = ["--json"];
@@ -2800,7 +2842,7 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
2800
2842
  flushGroup();
2801
2843
  blocks.push(buildDiagnosticBlock({
2802
2844
  kind: "total_limit",
2803
- message: `Search stopped after ${totalLimit} match event(s). Narrow the query to continue.`,
2845
+ message: `Search stopped after ${totalLimit} match event(s). Narrow the query, raise total_limit, or pass total_limit=0 to disable the cap.`,
2804
2846
  path: String(target).replace(/\\/g, "/")
2805
2847
  }));
2806
2848
  return blocks.map((block) => block.type === "edit_ready_block" ? serializeSearchBlock(block, { plain }) : serializeDiagnosticBlock(block)).join("\n\n");
@@ -3530,7 +3572,7 @@ function inspectPath(inputPath, opts = {}) {
3530
3572
  }
3531
3573
 
3532
3574
  // lib/setup.mjs
3533
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, mkdirSync, copyFileSync } from "node:fs";
3575
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, mkdirSync, copyFileSync, renameSync, unlinkSync } from "node:fs";
3534
3576
  import { resolve as resolve7, dirname as dirname4, join as join5 } from "node:path";
3535
3577
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3536
3578
  import { homedir } from "node:os";
@@ -3561,11 +3603,27 @@ var CLAUDE_HOOKS = {
3561
3603
  };
3562
3604
  function readJson(filePath) {
3563
3605
  if (!existsSync6(filePath)) return null;
3564
- return JSON.parse(readFileSync5(filePath, "utf-8"));
3606
+ try {
3607
+ return JSON.parse(readFileSync5(filePath, "utf-8"));
3608
+ } catch {
3609
+ process.stderr.write(`hex-line: warning \u2014 failed to parse ${filePath}, skipping
3610
+ `);
3611
+ return null;
3612
+ }
3565
3613
  }
3566
3614
  function writeJson(filePath, data) {
3567
3615
  mkdirSync(dirname4(filePath), { recursive: true });
3568
- writeFileSync2(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
3616
+ const tmp = `${filePath}.hexline-tmp-${process.pid}`;
3617
+ try {
3618
+ writeFileSync2(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
3619
+ renameSync(tmp, filePath);
3620
+ } catch (err) {
3621
+ try {
3622
+ unlinkSync(tmp);
3623
+ } catch {
3624
+ }
3625
+ throw err;
3626
+ }
3569
3627
  }
3570
3628
  function findEntryByCommand(entries) {
3571
3629
  return entries.findIndex(
@@ -3601,10 +3659,6 @@ function writeHooksToFile(settingsPath) {
3601
3659
  changed = true;
3602
3660
  }
3603
3661
  }
3604
- if (config.disableAllHooks !== false) {
3605
- config.disableAllHooks = false;
3606
- changed = true;
3607
- }
3608
3662
  if (changed) writeJson(settingsPath, config);
3609
3663
  return changed;
3610
3664
  }
@@ -4073,7 +4127,7 @@ async function fileChanges(filePath, compareAgainst = "HEAD") {
4073
4127
  }
4074
4128
 
4075
4129
  // lib/bulk-replace.mjs
4076
- import { writeFileSync as writeFileSync3, readdirSync as readdirSync3, renameSync, unlinkSync } from "node:fs";
4130
+ import { writeFileSync as writeFileSync3, readdirSync as readdirSync3, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "node:fs";
4077
4131
  import { resolve as resolve9, relative as relative4, join as join7 } from "node:path";
4078
4132
  var ignoreMod;
4079
4133
  try {
@@ -4149,10 +4203,10 @@ function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
4149
4203
  const tempPath = `${file}.hexline-tmp-${process.pid}`;
4150
4204
  try {
4151
4205
  writeFileSync3(tempPath, content, "utf-8");
4152
- renameSync(tempPath, file);
4206
+ renameSync2(tempPath, file);
4153
4207
  } catch (error) {
4154
4208
  try {
4155
- unlinkSync(tempPath);
4209
+ unlinkSync2(tempPath);
4156
4210
  } catch {
4157
4211
  }
4158
4212
  throw error;
@@ -4192,7 +4246,7 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
4192
4246
  }
4193
4247
 
4194
4248
  // server.mjs
4195
- var version = true ? "1.13.0" : (await null).createRequire(import.meta.url)("./package.json").version;
4249
+ var version = true ? "1.14.1" : (await null).createRequire(import.meta.url)("./package.json").version;
4196
4250
  var { server, StdioServerTransport } = await createServerRuntime({
4197
4251
  name: "hex-line-mcp",
4198
4252
  version
@@ -4261,12 +4315,14 @@ server.registerTool("edit_file", {
4261
4315
  dry_run: flexBool().describe("Preview changes without writing"),
4262
4316
  restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
4263
4317
  base_revision: z2.string().optional().describe("Prior revision from read_file/edit_file. Enables conservative auto-rebase for same-file follow-up edits."),
4264
- conflict_policy: z2.enum(["strict", "conservative"]).optional().describe('Conflict handling (default: "conservative"). "conservative" returns structured CONFLICT output with recovery_ranges, retry_edit/retry_edits, suggested_read_call, and retry_plan when available.')
4318
+ conflict_policy: z2.enum(["strict", "conservative"]).optional().describe('Conflict handling (default: "conservative"). "conservative" returns structured CONFLICT output with recovery_ranges, retry_edit/retry_edits, suggested_read_call, and retry_plan when available.'),
4319
+ allow_external: flexBool().describe("Allow editing a path outside the current project root. Use only when you intentionally target a temp or external file.")
4265
4320
  }),
4266
4321
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4267
4322
  }, async (rawParams) => {
4268
- const { path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy } = rawParams ?? {};
4323
+ const { path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy, allow_external } = rawParams ?? {};
4269
4324
  try {
4325
+ assertProjectScopedPath(p, { allowExternal: !!allow_external });
4270
4326
  let parsed;
4271
4327
  try {
4272
4328
  parsed = typeof json === "string" ? JSON.parse(json) : json;
@@ -4294,12 +4350,14 @@ server.registerTool("write_file", {
4294
4350
  description: "Create a new file or overwrite existing. Creates parent dirs. For existing files prefer edit_file (shows diff, verifies hashes).",
4295
4351
  inputSchema: z2.object({
4296
4352
  path: z2.string().describe("File path"),
4297
- content: z2.string().describe("File content")
4353
+ content: z2.string().describe("File content"),
4354
+ allow_external: flexBool().describe("Allow writing a path outside the current project root. Use only when you intentionally target a temp or external file.")
4298
4355
  }),
4299
4356
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
4300
4357
  }, async (rawParams) => {
4301
- const { path: p, content } = rawParams ?? {};
4358
+ const { path: p, content, allow_external } = rawParams ?? {};
4302
4359
  try {
4360
+ assertProjectScopedPath(p, { allowExternal: !!allow_external });
4303
4361
  const abs = validateWritePath(p);
4304
4362
  mkdirSync2(dirname6(abs), { recursive: true });
4305
4363
  writeFileSync4(abs, content, "utf-8");
@@ -4325,7 +4383,7 @@ server.registerTool("grep_search", {
4325
4383
  context_before: flexNum().describe("Context lines BEFORE match (-B)"),
4326
4384
  context_after: flexNum().describe("Context lines AFTER match (-A)"),
4327
4385
  limit: flexNum().describe("Max matches per file (default: 100)"),
4328
- total_limit: flexNum().describe("Total match events across all files; multiline matches count as 1 (0 = unlimited)"),
4386
+ total_limit: flexNum().describe("Total match events across all files; multiline matches count as 1 (default: 200 for content, 1000 for files/count, 0 = unlimited)"),
4329
4387
  plain: flexBool().describe("Omit hash tags, return file:line:content")
4330
4388
  }),
4331
4389
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
@@ -4450,12 +4508,14 @@ server.registerTool("bulk_replace", {
4450
4508
  path: z2.string().describe("Root directory for the replacement scope"),
4451
4509
  dry_run: flexBool().describe("Preview without writing (default: false)"),
4452
4510
  max_files: flexNum().describe("Max files to process (default: 100)"),
4453
- format: z2.enum(["compact", "full"]).optional().describe('"compact" (default) = summary only, "full" = include capped diffs')
4511
+ format: z2.enum(["compact", "full"]).optional().describe('"compact" (default) = summary only, "full" = include capped diffs'),
4512
+ allow_external: flexBool().describe("Allow a replacement root outside the current project root. Use only when you intentionally target a temp or external directory.")
4454
4513
  }),
4455
4514
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
4456
4515
  }, async (rawParams) => {
4457
4516
  try {
4458
4517
  const params = rawParams ?? {};
4518
+ assertProjectScopedPath(params.path, { allowExternal: !!params.allow_external });
4459
4519
  const raw = params.replacements;
4460
4520
  let replacementsInput;
4461
4521
  try {
package/output-style.md CHANGED
@@ -35,7 +35,9 @@ Prefer `hex-line` for text files you may inspect or modify. Hash-annotated reads
35
35
 
36
36
  - Auto-fill `path` instead of leaving scope implicit.
37
37
  - For file tools (`read_file`, `edit_file`, `outline`, `changes` on one file), use the target file path.
38
+ - Read-only file tools may target explicit temp-file paths outside the repo when you intentionally inspect a scratch file.
38
39
  - For repo-wide tools (`bulk_replace`, directory `inspect_path`, broad `grep_search`), use the resolved project root or intended directory scope.
40
+ - Mutating tools stay inside the current project root by default. Add `allow_external=true` only when you intentionally edit a temp or external path.
39
41
  - Treat missing or ambiguous scope as an error to fix, not as a reason to guess across repositories.
40
42
 
41
43
  ## Edit Discipline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levnikolaevich/hex-line-mcp",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "mcpName": "io.github.levnikolaevich/hex-line-mcp",
5
5
  "type": "module",
6
6
  "description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 9 tools: inspect_path, read, edit, write, grep, outline, verify, changes, bulk_replace.",