@getmarrow/mcp 3.9.103 → 3.9.107
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 +43 -6
- package/dist/approval-reply.d.ts +2 -0
- package/dist/approval-reply.d.ts.map +1 -0
- package/dist/approval-reply.js +18 -0
- package/dist/approval-reply.js.map +1 -0
- package/dist/cli.js +1677 -1636
- package/dist/cli.js.map +1 -1
- package/dist/codex-plugin-pins.d.ts.map +1 -1
- package/dist/codex-plugin-pins.js +18 -0
- package/dist/codex-plugin-pins.js.map +1 -1
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +83 -4
- package/dist/env.js.map +1 -1
- package/dist/hook-context.d.ts.map +1 -1
- package/dist/hook-context.js +25 -0
- package/dist/hook-context.js.map +1 -1
- package/dist/hook-contract.d.ts +4 -3
- package/dist/hook-contract.d.ts.map +1 -1
- package/dist/hook-contract.js +43 -6
- package/dist/hook-contract.js.map +1 -1
- package/dist/hook-pre-action.d.ts +43 -7
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +518 -21
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook-tool-policy.d.ts +86 -4
- package/dist/hook-tool-policy.d.ts.map +1 -1
- package/dist/hook-tool-policy.js +354 -21
- package/dist/hook-tool-policy.js.map +1 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +5 -3
- package/dist/hook.js.map +1 -1
- package/dist/host-approval-state.d.ts +24 -0
- package/dist/host-approval-state.d.ts.map +1 -1
- package/dist/host-approval-state.js +68 -0
- package/dist/host-approval-state.js.map +1 -1
- package/dist/host-approval.d.ts +22 -1
- package/dist/host-approval.d.ts.map +1 -1
- package/dist/host-approval.js +65 -4
- package/dist/host-approval.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/keyless-server.d.ts +53 -0
- package/dist/keyless-server.d.ts.map +1 -0
- package/dist/keyless-server.js +230 -0
- package/dist/keyless-server.js.map +1 -0
- package/dist/normalized-action.d.ts +11 -3
- package/dist/normalized-action.d.ts.map +1 -1
- package/dist/normalized-action.js +305 -49
- package/dist/normalized-action.js.map +1 -1
- package/dist/own-server-identity.d.ts +22 -3
- package/dist/own-server-identity.d.ts.map +1 -1
- package/dist/own-server-identity.js +108 -31
- package/dist/own-server-identity.js.map +1 -1
- package/dist/own-tool.d.ts +14 -6
- package/dist/own-tool.d.ts.map +1 -1
- package/dist/own-tool.js +18 -7
- package/dist/own-tool.js.map +1 -1
- package/dist/seatbelt/env-dump.d.ts +66 -0
- package/dist/seatbelt/env-dump.d.ts.map +1 -0
- package/dist/seatbelt/env-dump.js +333 -0
- package/dist/seatbelt/env-dump.js.map +1 -0
- package/dist/seatbelt/env-names.d.ts +22 -0
- package/dist/seatbelt/env-names.d.ts.map +1 -0
- package/dist/seatbelt/env-names.js +298 -0
- package/dist/seatbelt/env-names.js.map +1 -0
- package/dist/seatbelt/fallback-copy.d.ts +16 -0
- package/dist/seatbelt/fallback-copy.d.ts.map +1 -0
- package/dist/seatbelt/fallback-copy.js +68 -0
- package/dist/seatbelt/fallback-copy.js.map +1 -0
- package/dist/seatbelt/hook.d.ts +21 -2
- package/dist/seatbelt/hook.d.ts.map +1 -1
- package/dist/seatbelt/hook.js +45 -11
- package/dist/seatbelt/hook.js.map +1 -1
- package/dist/seatbelt/mapper.d.ts.map +1 -1
- package/dist/seatbelt/mapper.js +1592 -85
- package/dist/seatbelt/mapper.js.map +1 -1
- package/dist/seatbelt/tables.d.ts +1 -1
- package/dist/seatbelt/tables.d.ts.map +1 -1
- package/dist/seatbelt/tables.js +13 -17
- package/dist/seatbelt/tables.js.map +1 -1
- package/dist/secret-paths.d.ts +40 -0
- package/dist/secret-paths.d.ts.map +1 -0
- package/dist/secret-paths.js +59 -0
- package/dist/secret-paths.js.map +1 -0
- package/dist/session-loop-guard.d.ts.map +1 -1
- package/dist/session-loop-guard.js +25 -6
- package/dist/session-loop-guard.js.map +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/hook-tool-policy.js
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isOfficialMarrowMcpTool = exports.isOfficialMarrowMcpEvent = void 0;
|
|
3
|
+
exports.SHELL_DIALECT = exports.isOfficialMarrowMcpTool = exports.isOfficialMarrowMcpEvent = void 0;
|
|
4
4
|
exports.normalizeHookToolName = normalizeHookToolName;
|
|
5
5
|
exports.isMcpHookTool = isMcpHookTool;
|
|
6
6
|
exports.isProtectedShellMutation = isProtectedShellMutation;
|
|
7
7
|
exports.hookToolCommand = hookToolCommand;
|
|
8
8
|
exports.parseShellSegments = parseShellSegments;
|
|
9
9
|
exports.normalizedToolPath = normalizedToolPath;
|
|
10
|
+
exports.secretPatternRegex = secretPatternRegex;
|
|
10
11
|
exports.isSecretPath = isSecretPath;
|
|
11
12
|
exports.isProtectedWriteTarget = isProtectedWriteTarget;
|
|
12
13
|
exports.isSecretVariableName = isSecretVariableName;
|
|
13
14
|
exports.shellSegmentVerdict = shellSegmentVerdict;
|
|
15
|
+
exports.policyReadsAllWords = policyReadsAllWords;
|
|
14
16
|
exports.toolTargetPaths = toolTargetPaths;
|
|
15
17
|
exports.isSecretMaterialAccess = isSecretMaterialAccess;
|
|
18
|
+
exports.isSecretMaterialAccessAsNamed = isSecretMaterialAccessAsNamed;
|
|
16
19
|
exports.toolClassificationText = toolClassificationText;
|
|
20
|
+
exports.isShellTextTool = isShellTextTool;
|
|
17
21
|
exports.isShellGovernedTool = isShellGovernedTool;
|
|
18
22
|
exports.isReadOnlyToolEvent = isReadOnlyToolEvent;
|
|
23
|
+
exports.isReadOnlyToolEventAsNamed = isReadOnlyToolEventAsNamed;
|
|
24
|
+
exports.toolPolicyReading = toolPolicyReading;
|
|
25
|
+
exports.readEquivalent = readEquivalent;
|
|
26
|
+
exports.shellCommandToolText = shellCommandToolText;
|
|
27
|
+
exports.shellCommandToolRunOptions = shellCommandToolRunOptions;
|
|
28
|
+
exports.isWholeInputNativeTool = isWholeInputNativeTool;
|
|
29
|
+
const node_fs_1 = require("node:fs");
|
|
19
30
|
const node_os_1 = require("node:os");
|
|
20
31
|
const node_path_1 = require("node:path");
|
|
21
32
|
const own_tool_1 = require("./own-tool");
|
|
22
33
|
Object.defineProperty(exports, "isOfficialMarrowMcpEvent", { enumerable: true, get: function () { return own_tool_1.isOfficialMarrowMcpEvent; } });
|
|
23
34
|
Object.defineProperty(exports, "isOfficialMarrowMcpTool", { enumerable: true, get: function () { return own_tool_1.isOfficialMarrowMcpTool; } });
|
|
35
|
+
const secret_paths_1 = require("./secret-paths");
|
|
36
|
+
/**
|
|
37
|
+
* Marks the Bash twin of a PowerShell call (toolPolicyReading): its text is PowerShell, not Bash. A symbol,
|
|
38
|
+
* so no field of a host event or of a tool input (JSON has no symbols) can set or clear it.
|
|
39
|
+
*/
|
|
40
|
+
exports.SHELL_DIALECT = Symbol('marrow.shellDialect');
|
|
24
41
|
const READ_ONLY_TOOLS = new Set([
|
|
25
42
|
'read',
|
|
26
43
|
'read_file',
|
|
@@ -83,7 +100,9 @@ const SECRET_STORE_DIRECTORY = /^(?:\.ssh|\.gnupg|\.aws|\.azure|\.kube|\.docker|
|
|
|
83
100
|
// A plainly named secrets/credentials directory holds secret material, but source code may live in one too.
|
|
84
101
|
const NAMED_SECRET_DIRECTORY = /^(?:secrets?|credentials?)$/i;
|
|
85
102
|
const SECRET_FILE = /^(?:\.env(?:\..+)?|.+\.env|\.dev\.vars|\.git-credentials|\.netrc|_netrc|\.npmrc|\.yarnrc\.ya?ml|\.pypirc|\.pgpass|\.my\.cnf|\.htpasswd|\.?credentials\.(?:json|ya?ml|toml|ini|csv|txt|xml)|secrets?\.(?:json|ya?ml|toml|env|txt)|id_(?:rsa|dsa|ecdsa|ed25519)(?:_[A-Za-z0-9]+)?|.+\.(?:pem|key|p12|pfx|jks|keystore|kdbx|ppk))$/i;
|
|
86
|
-
|
|
103
|
+
// One list with the keyless seatbelt (ENV_TEMPLATE_WORDS, src/secret-paths.ts): a `.env` template is `.example`, `.sample` or `.template` and nothing else, so
|
|
104
|
+
// `.env.dist`, `.env.defaults` and `.env.default` are secret files in both modes. A test keeps this list equal to the keyless table's.
|
|
105
|
+
const ENV_TEMPLATE_FILE = new RegExp(`(?:^|\\.)env\\.(?:${secret_paths_1.ENV_TEMPLATE_WORDS.join('|')})$`, 'i');
|
|
87
106
|
const SECRET_KEYWORD_FILE = /(?:^|[._-])(?:secrets?|credentials?|tokens?|api[_-]?keys?|passwords?|private[_-]?keys?)(?:[._-]|$)/i;
|
|
88
107
|
const SOURCE_OR_DOC_FILE = /\.(?:[cm]?[jt]sx?|md|mdx|rst|py|rb|go|rs|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sh|bash|zsh|ps1|sql|html|css|scss|vue|svelte|map|snap|lock)$/i;
|
|
89
108
|
const SECRET_VARIABLE_PART = new Set(['KEY', 'APIKEY', 'TOKEN', 'SECRET', 'PASSWORD', 'PASSWD', 'PAT', 'CREDENTIAL', 'CREDENTIALS', 'AUTH']);
|
|
@@ -94,6 +113,9 @@ const MAX_SHELL_ANALYSIS_BYTES = 8192;
|
|
|
94
113
|
// or line separators as word characters, so splitting on them could hide one
|
|
95
114
|
// command inside another; such input takes the conservative whole-command path.
|
|
96
115
|
const UNMODELED_SHELL_CHARACTER = /[\u0000-\u0008\u000b-\u001f\u007f-\u00a0\u00ad\u061c\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u206f\u3000\ufeff\ufff9-\ufffb]/;
|
|
116
|
+
// PowerShell reads the typographic single and double quotation marks as quotes; Bash does not, so
|
|
117
|
+
// a PowerShell text holding one has quoting the splitter cannot follow.
|
|
118
|
+
const POWERSHELL_QUOTE_CHARACTER = /[\u2018-\u201f]/;
|
|
97
119
|
// Programs whose options can execute, write, or change what runs; an expanded
|
|
98
120
|
// variable could supply such an option, so they are not read-only with one.
|
|
99
121
|
const OPTION_SENSITIVE_PROGRAMS = new Set(['find', 'rg', 'sort', 'git', 'gh', 'date', 'env', 'file', 'npm', 'pnpm', 'yarn', 'node', 'npx',
|
|
@@ -186,9 +208,12 @@ function hookToolCommand(event) {
|
|
|
186
208
|
* the words alone (command or process substitution, subshells, heredocs,
|
|
187
209
|
* complex parameter expansion); callers then fall back to whole-command rules.
|
|
188
210
|
*/
|
|
189
|
-
function parseShellSegments(command) {
|
|
211
|
+
function parseShellSegments(command, dialect) {
|
|
190
212
|
if (command.length > MAX_SHELL_ANALYSIS_BYTES || UNMODELED_SHELL_CHARACTER.test(command))
|
|
191
213
|
return null;
|
|
214
|
+
const powershell = dialect === 'powershell';
|
|
215
|
+
if (powershell && POWERSHELL_QUOTE_CHARACTER.test(command))
|
|
216
|
+
return null;
|
|
192
217
|
const segments = [];
|
|
193
218
|
let segment = { words: [], outputs: [], inputs: [], expansions: [] };
|
|
194
219
|
let word = '';
|
|
@@ -271,7 +296,7 @@ function parseShellSegments(command) {
|
|
|
271
296
|
}
|
|
272
297
|
if (inner === '`')
|
|
273
298
|
return null;
|
|
274
|
-
if (inner === '\\' && index + 1 < command.length && '$`"\\\n'.includes(command[index + 1])) {
|
|
299
|
+
if (!powershell && inner === '\\' && index + 1 < command.length && '$`"\\\n'.includes(command[index + 1])) {
|
|
275
300
|
if (command[index + 1] !== '\n')
|
|
276
301
|
word += command[index + 1];
|
|
277
302
|
index += 2;
|
|
@@ -291,7 +316,8 @@ function parseShellSegments(command) {
|
|
|
291
316
|
return null;
|
|
292
317
|
continue;
|
|
293
318
|
}
|
|
294
|
-
|
|
319
|
+
// PowerShell's escape is the backtick (unmodeled); a backslash there is a path separator.
|
|
320
|
+
if (char === '\\' && !powershell) {
|
|
295
321
|
if (next === undefined)
|
|
296
322
|
return null;
|
|
297
323
|
if (next !== '\n') {
|
|
@@ -304,6 +330,9 @@ function parseShellSegments(command) {
|
|
|
304
330
|
// Braces expand or group commands, so the words would not be what runs.
|
|
305
331
|
if (char === '`' || char === '(' || char === ')' || char === '{')
|
|
306
332
|
return null;
|
|
333
|
+
// A PowerShell here-string (@' or @") holds lines of text up to a closing quote at a line start: not words.
|
|
334
|
+
if (powershell && char === '@' && (next === "'" || next === '"'))
|
|
335
|
+
return null;
|
|
307
336
|
if (char === '$') {
|
|
308
337
|
const consumed = expansion(index);
|
|
309
338
|
if (consumed < 0)
|
|
@@ -423,7 +452,7 @@ function secretPath(value, base) {
|
|
|
423
452
|
return true;
|
|
424
453
|
if (!SOURCE_OR_DOC_FILE.test(name) && directories.some((part) => NAMED_SECRET_DIRECTORY.test(part)))
|
|
425
454
|
return true;
|
|
426
|
-
if (
|
|
455
|
+
if (secret_paths_1.PROC_ENVIRON.test(path) || /^\/etc\/g?shadow$/.test(path))
|
|
427
456
|
return true;
|
|
428
457
|
// Claude Code local settings can carry agent keys in their env block.
|
|
429
458
|
if (name === 'settings.local.json' && parts[parts.length - 2] === '.claude')
|
|
@@ -434,8 +463,53 @@ function secretPath(value, base) {
|
|
|
434
463
|
return false;
|
|
435
464
|
if (SECRET_FILE.test(name))
|
|
436
465
|
return true;
|
|
466
|
+
// One table: every path the keyless seatbelt stops as a secret (S4) is a secret here too.
|
|
467
|
+
// Only the last 4096 characters (PATH_MAX) are read: the patterns end at `$`, and a two-`**`
|
|
468
|
+
// pattern backtracks quadratically, so an unbounded path could stall the hook past its timeout.
|
|
469
|
+
const tail = path.length > SECRET_PATH_TABLE_CHARS ? path.slice(-SECRET_PATH_TABLE_CHARS) : path;
|
|
470
|
+
if (SEATBELT_SECRET_PATHS.some((pattern) => pattern.test(tail)) || secret_paths_1.APPROVAL_STATE_TREE.test(tail))
|
|
471
|
+
return true;
|
|
437
472
|
return SECRET_KEYWORD_FILE.test(name) && !SOURCE_OR_DOC_FILE.test(name);
|
|
438
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* A SECRET_PATTERNS glob (src/secret-paths.ts) as a regex, the way the vendored classifier's globToRegex reads
|
|
476
|
+
* it (the table holds only `**`/`*` globs, home-agnostic `**`/ prefixes and absolute paths).
|
|
477
|
+
*/
|
|
478
|
+
function secretPatternRegex(pattern) {
|
|
479
|
+
let p = pattern;
|
|
480
|
+
let anchored = true;
|
|
481
|
+
if (p.startsWith('**/')) {
|
|
482
|
+
anchored = false;
|
|
483
|
+
p = p.slice(3);
|
|
484
|
+
}
|
|
485
|
+
let out = '';
|
|
486
|
+
for (let i = 0; i < p.length; i += 1) {
|
|
487
|
+
const ch = p[i];
|
|
488
|
+
if (ch === '*' && p[i + 1] === '*') {
|
|
489
|
+
if (p[i + 2] === '/') {
|
|
490
|
+
out += '(?:.*/)?';
|
|
491
|
+
i += 2;
|
|
492
|
+
}
|
|
493
|
+
else if (out.endsWith('/') && i + 2 === p.length) {
|
|
494
|
+
out = `${out.slice(0, -1)}(?:/.*)?`;
|
|
495
|
+
i += 1;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
out += '.*';
|
|
499
|
+
i += 1;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else if (ch === '*')
|
|
503
|
+
out += '[^/]*';
|
|
504
|
+
else if (ch === '?')
|
|
505
|
+
out += '[^/]';
|
|
506
|
+
else
|
|
507
|
+
out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
508
|
+
}
|
|
509
|
+
return new RegExp(`${anchored ? '^' : '(?:^|/)'}${out}$`);
|
|
510
|
+
}
|
|
511
|
+
const SEATBELT_SECRET_PATHS = secret_paths_1.SECRET_PATTERNS.map(secretPatternRegex);
|
|
512
|
+
const SECRET_PATH_TABLE_CHARS = 4096;
|
|
439
513
|
// A glob such as .env* or *.pem names secret files without spelling one out.
|
|
440
514
|
function globVariants(value) {
|
|
441
515
|
if (!/[*?[]/.test(value))
|
|
@@ -447,6 +521,37 @@ function globVariants(value) {
|
|
|
447
521
|
function isSecretPath(value, base = process.cwd()) {
|
|
448
522
|
return pathCandidates(String(value || '')).flatMap(globVariants).some((candidate) => secretPath(candidate, base));
|
|
449
523
|
}
|
|
524
|
+
/** The host-approvals folder, or ~/.marrow that holds it: a search tool that walks either reads the state file. */
|
|
525
|
+
function isApprovalStateFolder(value, base = process.cwd()) {
|
|
526
|
+
const folder = node_path_1.posix.join(homeDirectory(), '.marrow', 'host-approvals').toLowerCase();
|
|
527
|
+
return pathCandidates(String(value || '')).some((candidate) => {
|
|
528
|
+
const walked = normalizedToolPath(candidate, base).replace(/\/+$/, '').toLowerCase();
|
|
529
|
+
return walked === folder || walked === node_path_1.posix.dirname(folder);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/** A process folder (`/proc`, `/proc/self/task`): a search tool that walks it reads the `environ` of every process under it. */
|
|
533
|
+
function isProcessFolder(value, base = process.cwd()) {
|
|
534
|
+
return pathCandidates(String(value || '')).some((candidate) => secret_paths_1.PROC_PROCESS_TREE.test(normalizedToolPath(candidate, base)));
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* True when a path the tool names is a symbolic link, or goes through one, to a secret file (or, for a write, to a protected one): a read or an edit
|
|
538
|
+
* through it is of the file it points at, whatever the link is called. Only a path that exists and resolves elsewhere is read; nothing else changes.
|
|
539
|
+
*/
|
|
540
|
+
function secretThroughLink(value, write, base = process.cwd()) {
|
|
541
|
+
return pathCandidates(String(value || '')).some((candidate) => {
|
|
542
|
+
let real;
|
|
543
|
+
try {
|
|
544
|
+
const lexical = normalizedToolPath(candidate, base);
|
|
545
|
+
real = (0, node_fs_1.realpathSync)(lexical);
|
|
546
|
+
if (real === lexical)
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
return write ? isProtectedWriteTarget(real) : isSecretPath(real);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
450
555
|
/** True for a secret file or Marrow's own local control state. */
|
|
451
556
|
function isProtectedWriteTarget(value, base = process.cwd()) {
|
|
452
557
|
return pathCandidates(String(value || '')).flatMap(globVariants).some((candidate) => secretPath(candidate, base)
|
|
@@ -577,8 +682,15 @@ function readOnlyProgramInvocation(program, args) {
|
|
|
577
682
|
return !args.some((arg) => /^-[A-Za-z]*C/.test(arg) || longOptionPrefix(arg, ['compile']));
|
|
578
683
|
return true;
|
|
579
684
|
}
|
|
580
|
-
/**
|
|
581
|
-
|
|
685
|
+
/**
|
|
686
|
+
* Classifies one simple command: a read-only inspection, a secret access, or anything else.
|
|
687
|
+
* In the PowerShell dialect no command is a read-only inspection and no program is exempt from
|
|
688
|
+
* the content-read check (Bash's allowlists name Bash programs; `type` and `cat` read files in
|
|
689
|
+
* PowerShell, and `ls env:` and `echo $env:X` print the environment): every word is judged as a
|
|
690
|
+
* file a command may read.
|
|
691
|
+
*/
|
|
692
|
+
function shellSegmentVerdict(segment, cwd = process.cwd(), dialect) {
|
|
693
|
+
const powershell = dialect === 'powershell';
|
|
582
694
|
// After a cd Marrow cannot follow, relative paths are unknown, so nothing that follows is read-only.
|
|
583
695
|
const base = cwd ?? process.cwd();
|
|
584
696
|
const secret = (value) => isSecretPath(value, base);
|
|
@@ -587,7 +699,7 @@ function shellSegmentVerdict(segment, cwd = process.cwd()) {
|
|
|
587
699
|
if (segment.inputs.some(secret) || writes.some(protectedTarget))
|
|
588
700
|
return 'secret';
|
|
589
701
|
if (!segment.words.length)
|
|
590
|
-
return writes.length || cwd === null ? 'other' : 'read';
|
|
702
|
+
return writes.length || cwd === null || powershell ? 'other' : 'read';
|
|
591
703
|
// A leading assignment can redirect a pager or helper into arbitrary code.
|
|
592
704
|
const program = /^[A-Za-z_][A-Za-z0-9_]*=/.test(segment.words[0]) ? null : programName(segment.words[0]);
|
|
593
705
|
const args = segment.words.slice(1);
|
|
@@ -607,12 +719,15 @@ function shellSegmentVerdict(segment, cwd = process.cwd()) {
|
|
|
607
719
|
return 'secret';
|
|
608
720
|
if (program === 'cd' && args.some(secret))
|
|
609
721
|
return 'secret';
|
|
610
|
-
if (!NON_CONTENT_BASH_COMMANDS.has(program)) {
|
|
722
|
+
if (powershell || !NON_CONTENT_BASH_COMMANDS.has(program)) {
|
|
611
723
|
const targets = ['grep', 'egrep', 'fgrep', 'rg', 'jq'].includes(program) ? contentReadTargets(program, args) : args.filter((arg) => !arg.startsWith('-') || arg.includes('='));
|
|
612
724
|
if (targets.some(secret))
|
|
613
725
|
return 'secret';
|
|
614
726
|
}
|
|
615
|
-
|
|
727
|
+
// A PowerShell parameter binds its value in the same word (-Path:.env).
|
|
728
|
+
if (powershell && segment.words.some(protectedTarget))
|
|
729
|
+
return 'secret';
|
|
730
|
+
return writes.length || cwd === null || powershell ? 'other' : 'read';
|
|
616
731
|
}
|
|
617
732
|
return segment.words.some(protectedTarget) ? 'secret' : 'other';
|
|
618
733
|
}
|
|
@@ -627,18 +742,36 @@ function nextWorkingDirectory(segment, cwd) {
|
|
|
627
742
|
return null;
|
|
628
743
|
return normalizedToolPath(targets[0], cwd);
|
|
629
744
|
}
|
|
630
|
-
|
|
631
|
-
|
|
745
|
+
/** `parse`: the dialect the words are split in; `dialect`: the dialect the simple commands are judged in (default: the same). */
|
|
746
|
+
function shellAnalysis(command, dialect, parse = dialect) {
|
|
747
|
+
const segments = parseShellSegments(command, parse === 'bash' ? undefined : parse);
|
|
632
748
|
if (!segments)
|
|
633
749
|
return null;
|
|
634
750
|
let cwd = process.cwd();
|
|
635
751
|
const verdicts = segments.map((segment) => {
|
|
636
|
-
const verdict = shellSegmentVerdict(segment, cwd);
|
|
752
|
+
const verdict = shellSegmentVerdict(segment, cwd, dialect);
|
|
637
753
|
cwd = nextWorkingDirectory(segment, cwd);
|
|
638
754
|
return verdict;
|
|
639
755
|
});
|
|
640
756
|
return { segments, verdicts };
|
|
641
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* True when the policy's own reading is the whole answer for a shell command: it parses, every simple command in it is a recognised read-only
|
|
760
|
+
* program (a `cat`, `grep`, `ls`, `jq`, `ps`, never a wrapper such as `bash -c`, `eval`, `sudo`, `ssh`, `xargs` or a program that writes), and no word
|
|
761
|
+
* holds a glob, a brace or a variable (those can spell a name the text does not hold). Such a command names its files in the words the policy already
|
|
762
|
+
* judges, so a second reading of it by the mapper could only add the mapper's own over-stops (the pattern or filter operand of `grep .env README.md`).
|
|
763
|
+
*/
|
|
764
|
+
function policyReadsAllWords(command) {
|
|
765
|
+
try {
|
|
766
|
+
const analysis = shellAnalysis(command);
|
|
767
|
+
if (!analysis?.segments.length || !analysis.verdicts.every((verdict) => verdict === 'read'))
|
|
768
|
+
return false;
|
|
769
|
+
return analysis.segments.every((segment) => segment.expansions.length === 0 && ![...segment.words, ...segment.inputs, ...segment.outputs].some((word) => /[*?[{]/.test(word)));
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
642
775
|
function stringLeaves(value, depth = 0, output = []) {
|
|
643
776
|
if (output.length >= 64 || depth > 4)
|
|
644
777
|
return output;
|
|
@@ -684,18 +817,44 @@ function shellCommandText(event) {
|
|
|
684
817
|
/**
|
|
685
818
|
* True when a tool reads secret or credential material, or writes a secret file
|
|
686
819
|
* or Marrow's local control state. Unparseable shell falls back to a token scan.
|
|
820
|
+
* A Monitor command, and any shell text another tool carries, is read as Bash
|
|
821
|
+
* reads its command; a PowerShell command in the PowerShell dialect, which
|
|
822
|
+
* reads every word as a file the command may read (toolPolicyReading).
|
|
687
823
|
*/
|
|
688
|
-
function isSecretMaterialAccess(event) {
|
|
824
|
+
function isSecretMaterialAccess(event, printsEnvironment) {
|
|
825
|
+
const reading = toolPolicyReading(event);
|
|
826
|
+
if (reading.shell.some((twin) => isSecretMaterialAccessAsNamed(twin, printsEnvironment)))
|
|
827
|
+
return true;
|
|
828
|
+
if (reading.exact)
|
|
829
|
+
return false;
|
|
830
|
+
if (reading.base === 'read')
|
|
831
|
+
return isSecretMaterialAccessAsNamed(readEquivalent(event), printsEnvironment);
|
|
832
|
+
return reading.base === 'named' && isSecretMaterialAccessAsNamed(event, printsEnvironment);
|
|
833
|
+
}
|
|
834
|
+
/** isSecretMaterialAccess for the call's own tool name only (every tool before the built-ins were hooked). */
|
|
835
|
+
function isSecretMaterialAccessAsNamed(event, printsEnvironment) {
|
|
689
836
|
const tool = normalizeHookToolName(event.tool_name);
|
|
690
837
|
if (CONTENT_READ_TOOLS.has(tool))
|
|
691
|
-
return toolTargetPaths(event).some((path) => isSecretPath(path));
|
|
838
|
+
return toolTargetPaths(event).some((path) => isSecretPath(path) || secretThroughLink(path, false) || (tool === 'grep' && (isProcessFolder(path) || isApprovalStateFolder(path))));
|
|
692
839
|
if (FILE_EDIT_TOOLS.has(tool))
|
|
693
|
-
return toolTargetPaths(event).some((path) => isProtectedWriteTarget(path));
|
|
840
|
+
return toolTargetPaths(event).some((path) => isProtectedWriteTarget(path) || secretThroughLink(path, true));
|
|
694
841
|
if (!SHELL_TOOLS.has(tool))
|
|
695
842
|
return false;
|
|
696
843
|
const command = shellCommandText(event);
|
|
697
|
-
|
|
698
|
-
|
|
844
|
+
if (printsEnvironment && event[exports.SHELL_DIALECT] !== 'powershell' && printsEnvironment(command))
|
|
845
|
+
return true;
|
|
846
|
+
const analysis = shellAnalysis(command, event[exports.SHELL_DIALECT]);
|
|
847
|
+
if (event[exports.SHELL_DIALECT] === 'powershell') {
|
|
848
|
+
// The words are split as PowerShell splits them; text whose quoting Bash splits differently (an escaped quote,
|
|
849
|
+
// a typographic quote) is also split as Bash splits it, judged with no allowlist: a secret either finds is one.
|
|
850
|
+
// Where either split cannot be modelled, every token of the text is judged as a path.
|
|
851
|
+
const asBash = shellAnalysis(command, 'powershell', 'bash');
|
|
852
|
+
if (analysis?.verdicts.includes('secret') || asBash?.verdicts.includes('secret'))
|
|
853
|
+
return true;
|
|
854
|
+
if (analysis && asBash)
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
else if (analysis)
|
|
699
858
|
return analysis.verdicts.includes('secret');
|
|
700
859
|
return command.split(/[\s'"`;&|<>(){}]+/).some((token) => isProtectedWriteTarget(token));
|
|
701
860
|
}
|
|
@@ -712,7 +871,7 @@ function toolClassificationText(event) {
|
|
|
712
871
|
return [tool, ...toolTargetPaths(event)].join(' ');
|
|
713
872
|
const command = hookToolCommand(event);
|
|
714
873
|
if (SHELL_TOOLS.has(tool)) {
|
|
715
|
-
const analysis = shellAnalysis(shellCommandText(event));
|
|
874
|
+
const analysis = shellAnalysis(shellCommandText(event), event[exports.SHELL_DIALECT]);
|
|
716
875
|
if (analysis) {
|
|
717
876
|
return [tool, ...analysis.segments
|
|
718
877
|
.filter((_, index) => analysis.verdicts[index] !== 'read')
|
|
@@ -728,6 +887,17 @@ function toolClassificationText(event) {
|
|
|
728
887
|
}
|
|
729
888
|
return `${tool} ${command} ${serialized}`;
|
|
730
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Whether a tool runs its text as a shell command: Bash and the other terminal tools (Codex exec, Cursor Shell, ...),
|
|
892
|
+
* Monitor and PowerShell. Every other tool whose text is read as shell words (ToolSearch, a prompt, a path, an unknown
|
|
893
|
+
* tool's `command`) only borrows the parser, so no word of it is known to be a program.
|
|
894
|
+
*/
|
|
895
|
+
function isShellTextTool(event) {
|
|
896
|
+
if (isMcpHookTool(event.tool_name))
|
|
897
|
+
return false;
|
|
898
|
+
const tool = normalizeHookToolName(event.tool_name);
|
|
899
|
+
return SHELL_TOOLS.has(tool) || SHELL_COMMAND_TOOLS.has(tool);
|
|
900
|
+
}
|
|
731
901
|
/** Editing and task tools are judged by their target, not by shell-like text in their input. */
|
|
732
902
|
function isShellGovernedTool(event) {
|
|
733
903
|
const tool = normalizeHookToolName(event.tool_name);
|
|
@@ -742,14 +912,37 @@ function pathOnlyInput(value) {
|
|
|
742
912
|
&& keys.every((key) => ['path', 'file_path', 'filename', 'target_file'].includes(key))
|
|
743
913
|
&& Object.values(input).every((item) => typeof item === 'string' && item.trim().length > 0);
|
|
744
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* Whether a call only reads. A shell reading (Monitor, a native git
|
|
917
|
+
* equivalent) is read-only exactly when Bash running its text is; a PowerShell
|
|
918
|
+
* call never is (Bash's read-only commands are Bash's); any other
|
|
919
|
+
* call carrying shell text is read-only only when both its own reading and
|
|
920
|
+
* Bash running the text are (never more than before).
|
|
921
|
+
*/
|
|
745
922
|
function isReadOnlyToolEvent(event) {
|
|
923
|
+
const reading = toolPolicyReading(event);
|
|
924
|
+
if (reading.dialect === 'powershell')
|
|
925
|
+
return false;
|
|
926
|
+
const shellReadOnly = reading.shell.every((twin) => isReadOnlyToolEventAsNamed(twin));
|
|
927
|
+
if (reading.exact)
|
|
928
|
+
return reading.shell.length > 0 && shellReadOnly;
|
|
929
|
+
const own = reading.base === 'read' ? isReadOnlyToolEventAsNamed(readEquivalent(event))
|
|
930
|
+
: reading.base === 'non_acting' ? false
|
|
931
|
+
: isReadOnlyToolEventAsNamed(event);
|
|
932
|
+
return own && shellReadOnly;
|
|
933
|
+
}
|
|
934
|
+
/** isReadOnlyToolEvent for the call's own tool name only (every tool before the built-ins were hooked). */
|
|
935
|
+
function isReadOnlyToolEventAsNamed(event, printsEnvironment) {
|
|
746
936
|
const tool = normalizeHookToolName(event.tool_name);
|
|
747
937
|
if (!tool)
|
|
748
938
|
return false;
|
|
939
|
+
// Nothing PowerShell runs is a read-only inspection: the Bash twin of a PowerShell call never is.
|
|
940
|
+
if (event[exports.SHELL_DIALECT] === 'powershell')
|
|
941
|
+
return false;
|
|
749
942
|
if (LOCAL_TASK_TOOLS.has(tool))
|
|
750
943
|
return true;
|
|
751
944
|
if (READ_ONLY_TOOLS.has(tool))
|
|
752
|
-
return !isSecretMaterialAccess(event);
|
|
945
|
+
return !isSecretMaterialAccess(event, printsEnvironment);
|
|
753
946
|
if (['edit', 'write', 'multiedit', 'search_replace', 'run_terminal_command', 'spawn_subagent'].includes(tool))
|
|
754
947
|
return false;
|
|
755
948
|
if (MUTATION_TOOL_VERB.test(tool))
|
|
@@ -758,6 +951,9 @@ function isReadOnlyToolEvent(event) {
|
|
|
758
951
|
return true;
|
|
759
952
|
const command = hookToolCommand(event);
|
|
760
953
|
if (tool === 'bash' && command) {
|
|
954
|
+
// Printing the environment is never a read-only inspection.
|
|
955
|
+
if (printsEnvironment && printsEnvironment(shellCommandText(event)))
|
|
956
|
+
return false;
|
|
761
957
|
// A compound command is read-only only when every simple command in it is.
|
|
762
958
|
const analysis = shellAnalysis(shellCommandText(event));
|
|
763
959
|
return Boolean(analysis?.segments.length && analysis.verdicts.every((verdict) => verdict === 'read'));
|
|
@@ -766,4 +962,141 @@ function isReadOnlyToolEvent(event) {
|
|
|
766
962
|
&& pathOnlyInput(event.tool_input)
|
|
767
963
|
&& !toolTargetPaths(event).some((path) => isSecretPath(path));
|
|
768
964
|
}
|
|
965
|
+
// ---------------------------------------------------------------------------
|
|
966
|
+
// How a call is read for classification. Keys always keep the call's
|
|
967
|
+
// own tool name and input (normalized-action, hook-contract).
|
|
968
|
+
// ---------------------------------------------------------------------------
|
|
969
|
+
// Claude Code tools that run their `command` (or `script`) as a shell command: Monitor in the same
|
|
970
|
+
// shell environment as Bash, PowerShell in PowerShell. Any case or padding of the name. Monitor's
|
|
971
|
+
// text goes through Bash's rules exactly. PowerShell's does not: Bash's allowlists name Bash
|
|
972
|
+
// programs (`type`, `cat`, `find`, `ls`, `echo` read or print different things in PowerShell), so a
|
|
973
|
+
// PowerShell call is never read-only and is classified as the stricter of its own (named) reading
|
|
974
|
+
// and Bash's reading of its text with no allowlist applied (the PowerShell dialect of the splitter).
|
|
975
|
+
const SHELL_COMMAND_TOOLS = new Set(['monitor', 'powershell']);
|
|
976
|
+
// How long a Monitor or PowerShell call may run and whether it runs in the background: they are
|
|
977
|
+
// part of what is approved (a short-timeout approval is not a long-timeout one).
|
|
978
|
+
const SHELL_COMMAND_TOOL_RUN_FIELDS = ['timeout', 'timeout_ms', 'run_in_background'];
|
|
979
|
+
// The fields a Monitor or PowerShell call may carry besides its shell text without changing what runs.
|
|
980
|
+
const SHELL_COMMAND_TOOL_FIELDS = new Set(['description', ...SHELL_COMMAND_TOOL_RUN_FIELDS]);
|
|
981
|
+
// Tools whose `command` names something other than shell text (a slash command, a skill, an editor
|
|
982
|
+
// operation), and Workflow, whose `script` is JavaScript that only starts sub-agents.
|
|
983
|
+
const NON_SHELL_COMMAND_TOOLS = new Set(['slashcommand', 'skill', 'str_replace_editor', 'str_replace_based_edit_tool', 'workflow']);
|
|
984
|
+
// Claude Code built-ins that run a fixed local git command (exact host label): read as Bash running it.
|
|
985
|
+
const NATIVE_SHELL_EQUIVALENTS = new Map([
|
|
986
|
+
['EnterWorktree', 'git worktree add'],
|
|
987
|
+
['ExitWorktree', 'git worktree remove'],
|
|
988
|
+
]);
|
|
989
|
+
// Claude Code built-ins no released Marrow matcher hooked that never act themselves (exact host
|
|
990
|
+
// label): what they lead to (a sub-agent's tools, a scheduled prompt, the agent's next step after a
|
|
991
|
+
// fetch, a message or a question) reaches the hook on its own. As for task bookkeeping, words in
|
|
992
|
+
// their prompt, message, plan or URL are not performed by the call, so they never make it
|
|
993
|
+
// protected; the call is still reported to Marrow (never read-only). Monitor is here only without
|
|
994
|
+
// shell text (its WebSocket source). The tools that only report, notify, structure output, render or
|
|
995
|
+
// hand text back, and what each one does:
|
|
996
|
+
// Artifact, ArtifactCheck, ArtifactComments, ArtifactData: the host's artifact pane (render a page
|
|
997
|
+
// for the user, check it, comment on it, give it data); no command, no local file.
|
|
998
|
+
// ListAgents: lists the agents SendMessage can reach.
|
|
999
|
+
// PushNotification: a desktop or phone notification with a one-line `message`.
|
|
1000
|
+
// ReportFindings: hands a code review's findings to the host UI to render.
|
|
1001
|
+
// StructuredOutput: hands the final structured result back to the caller.
|
|
1002
|
+
// SubagentHandback: hands a sub-agent's text back to the agent that started it.
|
|
1003
|
+
// Workflow: its `script` (JavaScript) only starts sub-agents, whose own tools reach the hook.
|
|
1004
|
+
// Tools a released matcher already hooked keep their reading (BashOutput, NotebookRead,
|
|
1005
|
+
// ReadMcpResourceTool, Task*, TodoWrite, ToolSearch); RemoteTrigger and DesignSync act on remote
|
|
1006
|
+
// services and stay as named.
|
|
1007
|
+
const NON_ACTING_NATIVE_TOOLS = new Set(['Agent', 'SendMessage', 'WebFetch', 'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode',
|
|
1008
|
+
'SlashCommand', 'CronCreate', 'CronDelete', 'CronList', 'ScheduleWakeup', 'KillShell', 'Monitor',
|
|
1009
|
+
'Artifact', 'ArtifactCheck', 'ArtifactComments', 'ArtifactData', 'ListAgents', 'PushNotification', 'ReportFindings',
|
|
1010
|
+
'StructuredOutput', 'SubagentHandback', 'Workflow']);
|
|
1011
|
+
// Claude Code's Skill loads a skill's instructions (exact host label): read as Read.
|
|
1012
|
+
const READ_EQUIVALENT_NATIVE_TOOLS = new Set(['Skill']);
|
|
1013
|
+
function bashTwin(command) {
|
|
1014
|
+
return { tool_name: 'Bash', tool_input: { command } };
|
|
1015
|
+
}
|
|
1016
|
+
function powerShellTwin(command) {
|
|
1017
|
+
return { tool_name: 'Bash', tool_input: { command }, [exports.SHELL_DIALECT]: 'powershell' };
|
|
1018
|
+
}
|
|
1019
|
+
/** The string `command` and `script` texts of a call that are not blank (both when both are set). */
|
|
1020
|
+
function shellTexts(event) {
|
|
1021
|
+
const input = asRecord(event.tool_input);
|
|
1022
|
+
if (!input)
|
|
1023
|
+
return [];
|
|
1024
|
+
const texts = [];
|
|
1025
|
+
for (const key of ['command', 'script']) {
|
|
1026
|
+
const value = input[key];
|
|
1027
|
+
if (typeof value === 'string' && value.trim() && !texts.includes(value))
|
|
1028
|
+
texts.push(value);
|
|
1029
|
+
}
|
|
1030
|
+
return texts;
|
|
1031
|
+
}
|
|
1032
|
+
function toolPolicyReading(event) {
|
|
1033
|
+
const named = { base: 'named', shell: [], exact: false, bashLabel: false };
|
|
1034
|
+
const tool = normalizeHookToolName(event.tool_name);
|
|
1035
|
+
// Bash and the other shell tools are read as they always were; Marrow's own and MCP calls keep their reading plus any shell text.
|
|
1036
|
+
if (!tool || SHELL_TOOLS.has(tool))
|
|
1037
|
+
return named;
|
|
1038
|
+
const label = typeof event.tool_name === 'string' ? event.tool_name : '';
|
|
1039
|
+
const texts = NON_SHELL_COMMAND_TOOLS.has(tool) ? [] : shellTexts(event);
|
|
1040
|
+
const equivalent = isMcpHookTool(event.tool_name) ? undefined : NATIVE_SHELL_EQUIVALENTS.get(label);
|
|
1041
|
+
if (equivalent !== undefined)
|
|
1042
|
+
return { base: 'named', shell: [equivalent, ...texts].map(bashTwin), exact: true, bashLabel: false };
|
|
1043
|
+
if (tool === 'monitor' && texts.length && !isMcpHookTool(event.tool_name)) {
|
|
1044
|
+
return { base: 'named', shell: texts.map(bashTwin), exact: true, bashLabel: true };
|
|
1045
|
+
}
|
|
1046
|
+
// PowerShell: the stricter of its named reading and Bash's reading of the text in the PowerShell dialect.
|
|
1047
|
+
if (tool === 'powershell' && texts.length && !isMcpHookTool(event.tool_name)) {
|
|
1048
|
+
return { base: 'named', shell: texts.map(powerShellTwin), exact: false, bashLabel: false, dialect: 'powershell' };
|
|
1049
|
+
}
|
|
1050
|
+
const base = isMcpHookTool(event.tool_name) ? 'named'
|
|
1051
|
+
: READ_EQUIVALENT_NATIVE_TOOLS.has(label) ? 'read'
|
|
1052
|
+
: NON_ACTING_NATIVE_TOOLS.has(label) ? 'non_acting'
|
|
1053
|
+
: 'named';
|
|
1054
|
+
return { base, shell: texts.map(bashTwin), exact: false, bashLabel: false };
|
|
1055
|
+
}
|
|
1056
|
+
/** The Read call a Read-equivalent tool is read as (same input). */
|
|
1057
|
+
function readEquivalent(event) {
|
|
1058
|
+
return { tool_name: 'Read', tool_input: event.tool_input };
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* The shell text of a plain Monitor or PowerShell call (one command or script string, no field
|
|
1062
|
+
* that changes what runs besides it), else null. Only such a call is keyed as shell text.
|
|
1063
|
+
*/
|
|
1064
|
+
function shellCommandToolText(event) {
|
|
1065
|
+
if (isMcpHookTool(event.tool_name) || !SHELL_COMMAND_TOOLS.has(normalizeHookToolName(event.tool_name)))
|
|
1066
|
+
return null;
|
|
1067
|
+
const input = asRecord(event.tool_input);
|
|
1068
|
+
if (!input)
|
|
1069
|
+
return null;
|
|
1070
|
+
const texts = Object.keys(input).filter((key) => !SHELL_COMMAND_TOOL_FIELDS.has(key));
|
|
1071
|
+
if (texts.length !== 1 || !['command', 'script'].includes(texts[0]))
|
|
1072
|
+
return null;
|
|
1073
|
+
const value = input[texts[0]];
|
|
1074
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* The run options (`timeout`, `timeout_ms`, `run_in_background`) a Monitor or PowerShell call carries, in a
|
|
1078
|
+
* fixed order and as sent, or null for any other call or when it carries none. They are part of the key
|
|
1079
|
+
* of the call (normalized-action, hook-contract exactHookTarget); `description` is not.
|
|
1080
|
+
*/
|
|
1081
|
+
function shellCommandToolRunOptions(event) {
|
|
1082
|
+
if (isMcpHookTool(event.tool_name) || !SHELL_COMMAND_TOOLS.has(normalizeHookToolName(event.tool_name)))
|
|
1083
|
+
return null;
|
|
1084
|
+
const input = asRecord(event.tool_input);
|
|
1085
|
+
if (!input)
|
|
1086
|
+
return null;
|
|
1087
|
+
const options = {};
|
|
1088
|
+
for (const key of SHELL_COMMAND_TOOL_RUN_FIELDS) {
|
|
1089
|
+
if (Object.prototype.hasOwnProperty.call(input, key))
|
|
1090
|
+
options[key] = input[key];
|
|
1091
|
+
}
|
|
1092
|
+
return Object.keys(options).length ? options : null;
|
|
1093
|
+
}
|
|
1094
|
+
/** A Claude Code built-in keyed by its whole input (a native git equivalent, Skill, or a call whose words are not actions). */
|
|
1095
|
+
function isWholeInputNativeTool(event) {
|
|
1096
|
+
const reading = toolPolicyReading(event);
|
|
1097
|
+
if ((reading.exact && !reading.bashLabel) || reading.base !== 'named')
|
|
1098
|
+
return true;
|
|
1099
|
+
// A Monitor or PowerShell call is keyed as shell text only when it is a plain one.
|
|
1100
|
+
return ((reading.exact && reading.bashLabel) || reading.dialect === 'powershell') && shellCommandToolText(event) === null;
|
|
1101
|
+
}
|
|
769
1102
|
//# sourceMappingURL=hook-tool-policy.js.map
|