tollgate-cli 0.2.1 → 0.2.3

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
@@ -34,6 +34,16 @@ Every other command runs untouched; the hook adds about 50 ms.
34
34
  | `.claude/settings.local.json` | No (added to `.gitignore`): your personal hook registration |
35
35
  | `~/.config/tollgate/` | Never in a repo: your token (file mode 0600) and the hook bundle |
36
36
 
37
+ ## What the hook can see through
38
+
39
+ Agents rarely type the risky command itself. The hook looks at what a command really runs:
40
+
41
+ - **Package scripts:** `pnpm db:deploy`, `npm run release`, `yarn deploy`, `bun run x` are looked up in the nearest `package.json` (with `pre`/`post` scripts and scripts that call other scripts), so the card shows _pnpm db:deploy → prisma migrate deploy_ with the migration SQL.
42
+ - **Lists and wrappers:** every part of `a && b; c | d` is checked on its own, so `npm publish && echo --dry-run` is still a publish. Prefixes like `VAR=x`, `env`, `sudo`, `npx`, `pnpm exec`, `dotenv --` and literal `bash -c '…'`, `eval '…'`, `( … )` and `$( … )` are looked inside.
43
+ - **Code nobody can read first:** `curl … | sh`, `base64 -d | sh`, `bash <(curl …)` and commands built from a variable (`eval "$CMD"`) are gated as _opaque_.
44
+
45
+ Tollgate is not a shell parser and not a sandbox. It does **not** read Makefiles (`make deploy`), shell files (`./deploy.sh`), npm lifecycle scripts like `prepublishOnly`, or workspace scripts selected with `--filter`. It guards against an agent's mistakes, not against someone determined to get around it on their own machine.
46
+
37
47
  ## What leaves your machine
38
48
 
39
49
  Only for commands that match a preset: the command and working directory after **local redaction** (known token formats, `KEY=value` secrets, passwords in URLs, secret flags, and every value from your `.env*` files), Claude Code's one-line description of the command (shown to approvers as the agent's reason), your git branch, the last commit subject, and for migrations the newest migration's SQL (capped at 2 KB). The card shows how many values were hidden. The server redacts everything again.
package/dist/tollgate.mjs CHANGED
@@ -5242,14 +5242,14 @@ var memo = {
5242
5242
  var _a3;
5243
5243
  (_a3 = inst._zod).deferred ?? (_a3.deferred = []);
5244
5244
  inst._zod.deferred.push(() => {
5245
- const base = inst._zod.parse;
5245
+ const base2 = inst._zod.parse;
5246
5246
  const wrapped = (payload, ctx) => {
5247
5247
  if (ctx.direction !== "backward" && isBackEdge(ctx, payload.value))
5248
5248
  throw new $ZodCyclicError();
5249
- return base(payload, ctx);
5249
+ return base2(payload, ctx);
5250
5250
  };
5251
5251
  inst._zod.parse = wrapped;
5252
- if (inst._zod.run === base)
5252
+ if (inst._zod.run === base2)
5253
5253
  inst._zod.run = wrapped;
5254
5254
  });
5255
5255
  },
@@ -5261,15 +5261,15 @@ var memo = {
5261
5261
  let lastBucket;
5262
5262
  (_a3 = inst._zod).deferred ?? (_a3.deferred = []);
5263
5263
  inst._zod.deferred.push(() => {
5264
- const base = inst._zod.parse;
5264
+ const base2 = inst._zod.parse;
5265
5265
  const wrapped = (payload, ctx) => {
5266
5266
  if (isRecursiveInst === void 0) {
5267
5267
  const walked = isRecursive(inst, /* @__PURE__ */ new Set(), false);
5268
5268
  if (walked === NONE) {
5269
- inst._zod.parse = base;
5269
+ inst._zod.parse = base2;
5270
5270
  if (inst._zod.run === wrapped)
5271
- inst._zod.run = base;
5272
- return base(payload, ctx);
5271
+ inst._zod.run = base2;
5272
+ return base2(payload, ctx);
5273
5273
  }
5274
5274
  if (walked === PROVEN || rechecked)
5275
5275
  isRecursiveInst = true;
@@ -5278,7 +5278,7 @@ var memo = {
5278
5278
  }
5279
5279
  const input2 = payload.value;
5280
5280
  if (!isRef(input2))
5281
- return base(payload, ctx);
5281
+ return base2(payload, ctx);
5282
5282
  let state = ctx[STATE];
5283
5283
  if (!state) {
5284
5284
  state = { buckets: /* @__PURE__ */ new WeakMap(), backEdges: void 0 };
@@ -5307,7 +5307,7 @@ var memo = {
5307
5307
  }
5308
5308
  handoff = bucket;
5309
5309
  const depth = open.length;
5310
- const result = base(payload, ctx);
5310
+ const result = base2(payload, ctx);
5311
5311
  handoff = void 0;
5312
5312
  const entry = open.length > depth ? open.pop() : void 0;
5313
5313
  if (result instanceof Promise) {
@@ -5322,7 +5322,7 @@ var memo = {
5322
5322
  return result;
5323
5323
  };
5324
5324
  inst._zod.parse = wrapped;
5325
- if (inst._zod.run === base)
5325
+ if (inst._zod.run === base2)
5326
5326
  inst._zod.run = wrapped;
5327
5327
  });
5328
5328
  }
@@ -13231,9 +13231,9 @@ function codePointLengthVar(doc, ctx, accessor, inDoubt) {
13231
13231
  doc.write(`const ${v} = typeof ${accessor} === "string" && ${inDoubt} ? ${cpLen}(${accessor}) : ${accessor}.length;`);
13232
13232
  return v;
13233
13233
  }
13234
- function numericOperand(value, label) {
13234
+ function numericOperand(value, label2) {
13235
13235
  if (typeof value !== "number" || !Number.isFinite(value)) {
13236
- throw new ZodCompileUnsupportedError(`${label} bound of type ${typeof value}`);
13236
+ throw new ZodCompileUnsupportedError(`${label2} bound of type ${typeof value}`);
13237
13237
  }
13238
13238
  return `${value}`;
13239
13239
  }
@@ -19727,6 +19727,337 @@ function date4(params) {
19727
19727
  return _coercedDate(ZodDate, params);
19728
19728
  }
19729
19729
 
19730
+ // ../../features/gates/shared/expand.ts
19731
+ var MAX_DEPTH = 4;
19732
+ var MAX_SEGMENTS = 64;
19733
+ var MAX_LABEL = 80;
19734
+ function scan(text, onChar) {
19735
+ const state = { quote: null, depth: 0, backtick: false };
19736
+ for (let i = 0; i < text.length; i++) {
19737
+ const ch = text[i];
19738
+ const skip = onChar(i, state);
19739
+ if (typeof skip === "number") {
19740
+ i = skip;
19741
+ continue;
19742
+ }
19743
+ if (state.quote === "'") {
19744
+ if (ch === "'") state.quote = null;
19745
+ continue;
19746
+ }
19747
+ if (ch === "\\") {
19748
+ i += 1;
19749
+ continue;
19750
+ }
19751
+ if (state.quote === '"') {
19752
+ if (ch === '"') state.quote = null;
19753
+ else if (ch === "$" && text[i + 1] === "(") {
19754
+ state.depth += 1;
19755
+ i += 1;
19756
+ } else if (ch === ")" && state.depth > 0) state.depth -= 1;
19757
+ continue;
19758
+ }
19759
+ if (ch === "'" || ch === '"') state.quote = ch;
19760
+ else if (ch === "`") state.backtick = !state.backtick;
19761
+ else if (ch === "(") state.depth += 1;
19762
+ else if (ch === ")" && state.depth > 0) state.depth -= 1;
19763
+ }
19764
+ }
19765
+ var atTop = (s) => s.quote === null && s.depth === 0 && !s.backtick;
19766
+ function splitList(text) {
19767
+ const parts = [];
19768
+ let start = 0;
19769
+ scan(text, (i, s) => {
19770
+ if (!atTop(s)) return;
19771
+ const ch = text[i];
19772
+ const next = text[i + 1];
19773
+ const prev = text[i - 1];
19774
+ if (ch === "#" && (i === 0 || /\s/.test(prev))) {
19775
+ parts.push(text.slice(start, i));
19776
+ const end = text.indexOf("\n", i);
19777
+ start = end === -1 ? text.length : end + 1;
19778
+ return end === -1 ? text.length : end;
19779
+ }
19780
+ if (ch === ";" || ch === "\n") {
19781
+ parts.push(text.slice(start, i));
19782
+ start = i + 1;
19783
+ } else if (ch === "&" && next === "&" || ch === "|" && next === "|") {
19784
+ parts.push(text.slice(start, i));
19785
+ start = i + 2;
19786
+ return i + 1;
19787
+ } else if (ch === "&" && prev !== ">" && prev !== "<" && prev !== "|" && next !== ">") {
19788
+ parts.push(text.slice(start, i));
19789
+ start = i + 1;
19790
+ }
19791
+ });
19792
+ parts.push(text.slice(start));
19793
+ return parts.map((part) => part.trim()).filter(Boolean);
19794
+ }
19795
+ function splitPipeline(text) {
19796
+ const parts = [];
19797
+ let start = 0;
19798
+ scan(text, (i, s) => {
19799
+ if (!atTop(s) || text[i] !== "|") return;
19800
+ parts.push(text.slice(start, i));
19801
+ const skipAmp = text[i + 1] === "&";
19802
+ start = i + (skipAmp ? 2 : 1);
19803
+ return skipAmp ? i + 1 : void 0;
19804
+ });
19805
+ parts.push(text.slice(start));
19806
+ return parts.map((part) => part.trim()).filter(Boolean);
19807
+ }
19808
+ function shellWords(text) {
19809
+ const words = [];
19810
+ let current = "";
19811
+ let inWord = false;
19812
+ let quote = null;
19813
+ let depth = 0;
19814
+ let backtick = false;
19815
+ for (let i = 0; i < text.length; i++) {
19816
+ const ch = text[i];
19817
+ if (quote === "'") {
19818
+ if (ch === "'") quote = null;
19819
+ else current += ch;
19820
+ continue;
19821
+ }
19822
+ if (ch === "\\" && i + 1 < text.length) {
19823
+ current += text[i + 1];
19824
+ inWord = true;
19825
+ i += 1;
19826
+ continue;
19827
+ }
19828
+ if (quote === '"') {
19829
+ if (ch === '"' && depth === 0) quote = null;
19830
+ else {
19831
+ if (ch === "$" && text[i + 1] === "(") depth += 1;
19832
+ else if (ch === ")" && depth > 0) depth -= 1;
19833
+ current += ch;
19834
+ }
19835
+ continue;
19836
+ }
19837
+ if (depth > 0 || backtick) {
19838
+ if (ch === "(") depth += 1;
19839
+ else if (ch === ")") depth -= 1;
19840
+ else if (ch === "`") backtick = false;
19841
+ current += ch;
19842
+ continue;
19843
+ }
19844
+ if (/\s/.test(ch)) {
19845
+ if (inWord) words.push(current);
19846
+ current = "";
19847
+ inWord = false;
19848
+ continue;
19849
+ }
19850
+ inWord = true;
19851
+ if (ch === "'" || ch === '"') quote = ch;
19852
+ else if (ch === "$" && text[i + 1] === "(") {
19853
+ depth = 1;
19854
+ current += "$(";
19855
+ i += 1;
19856
+ } else if (ch === "`") {
19857
+ backtick = true;
19858
+ current += ch;
19859
+ } else current += ch;
19860
+ }
19861
+ if (inWord) words.push(current);
19862
+ return words;
19863
+ }
19864
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
19865
+ var base = (word) => word.slice(word.lastIndexOf("/") + 1);
19866
+ function skipFlags(words, withValue = /* @__PURE__ */ new Set()) {
19867
+ let i = 0;
19868
+ while (i < words.length && words[i].startsWith("-") && words[i] !== "--") {
19869
+ i += withValue.has(words[i]) ? 2 : 1;
19870
+ }
19871
+ if (words[i] === "--") i += 1;
19872
+ return words.slice(i);
19873
+ }
19874
+ var SUDO_VALUE = /* @__PURE__ */ new Set(["-u", "-g", "-C", "-D", "-h", "-p", "-r", "-t", "-U", "-T"]);
19875
+ var NPX_VALUE = /* @__PURE__ */ new Set(["-p", "--package", "-c", "--call"]);
19876
+ var ENV_VALUE = /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir", "-S", "--split-string"]);
19877
+ function stripPrefixes(input2) {
19878
+ let words = input2;
19879
+ for (let guard = 0; guard < 16 && words.length > 0; guard++) {
19880
+ const first = base(words[0]);
19881
+ const second = words[1];
19882
+ let next = null;
19883
+ if (ASSIGNMENT.test(words[0])) next = words.slice(1);
19884
+ else if (first === "env") {
19885
+ next = skipFlags(words.slice(1), ENV_VALUE);
19886
+ while (next.length > 0 && ASSIGNMENT.test(next[0])) next = next.slice(1);
19887
+ } else if (first === "sudo" || first === "doas") next = skipFlags(words.slice(1), SUDO_VALUE);
19888
+ else if (["time", "nohup", "command", "exec", "builtin"].includes(first)) {
19889
+ next = skipFlags(words.slice(1));
19890
+ } else if (first === "nice") next = skipFlags(words.slice(1), /* @__PURE__ */ new Set(["-n"]));
19891
+ else if (first === "npx" || first === "bunx") next = skipFlags(words.slice(1), NPX_VALUE);
19892
+ else if (first === "pnpm" && (second === "exec" || second === "dlx")) {
19893
+ next = skipFlags(words.slice(2));
19894
+ } else if (first === "yarn" && (second === "dlx" || second === "exec")) {
19895
+ next = skipFlags(words.slice(2));
19896
+ } else if (first === "npm" && (second === "exec" || second === "x")) {
19897
+ next = skipFlags(words.slice(2), NPX_VALUE);
19898
+ } else if (first === "dotenv" || first === "dotenvx") {
19899
+ const dash = words.indexOf("--");
19900
+ next = dash === -1 ? skipFlags(words.slice(1), /* @__PURE__ */ new Set(["-e", "-f", "-v", "-c"])) : words.slice(dash + 1);
19901
+ if (first === "dotenvx" && next[0] === "run")
19902
+ next = skipFlags(next.slice(1), /* @__PURE__ */ new Set(["-f"]));
19903
+ } else if (first === "cross-env") {
19904
+ next = words.slice(1);
19905
+ while (next.length > 0 && ASSIGNMENT.test(next[0])) next = next.slice(1);
19906
+ }
19907
+ if (next === null) break;
19908
+ words = next;
19909
+ }
19910
+ return words;
19911
+ }
19912
+ var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh"]);
19913
+ function innerScript(words) {
19914
+ if (words.length === 0) return null;
19915
+ const first = base(words[0]);
19916
+ if (SHELLS.has(first)) {
19917
+ const flag = words.findIndex((word, i) => i > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(word));
19918
+ return flag !== -1 && words[flag + 1] !== void 0 ? words[flag + 1] : null;
19919
+ }
19920
+ if (first === "eval") return words.slice(1).join(" ") || null;
19921
+ return null;
19922
+ }
19923
+ function substitutions(text) {
19924
+ const found = [];
19925
+ for (let i = 0; i < text.length; i++) {
19926
+ if (text[i] === "'") {
19927
+ const end = text.indexOf("'", i + 1);
19928
+ if (end === -1) break;
19929
+ i = end;
19930
+ continue;
19931
+ }
19932
+ if (text[i] === "$" && text[i + 1] === "(" && text[i + 2] !== "(") {
19933
+ let depth = 1;
19934
+ let j = i + 2;
19935
+ for (; j < text.length && depth > 0; j++) {
19936
+ if (text[j] === "(") depth += 1;
19937
+ else if (text[j] === ")") depth -= 1;
19938
+ }
19939
+ found.push(text.slice(i + 2, j - 1));
19940
+ i = j - 1;
19941
+ } else if (text[i] === "`") {
19942
+ const end = text.indexOf("`", i + 1);
19943
+ if (end === -1) break;
19944
+ found.push(text.slice(i + 1, end));
19945
+ i = end;
19946
+ }
19947
+ }
19948
+ return found.filter((inner) => inner.trim());
19949
+ }
19950
+ var PNPM_BUILTINS = new Set(
19951
+ "add i install update up upgrade remove rm uninstall un link ln unlink import rebuild rb prune fetch patch patch-commit exec dlx create publish pack audit list ls outdated why store root bin config c get set env init setup server deploy licenses doctor approve-builds ignored-builds self-update cat-file cat-index find-hash recursive multi m help".split(
19952
+ " "
19953
+ )
19954
+ );
19955
+ var YARN_BUILTINS = new Set(
19956
+ "add install remove upgrade up publish init info why dlx exec workspace workspaces config cache link unlink pack version global import npm plugin set constraints dedupe explain node rebuild search stage unplug patch patch-commit login logout owner tag team audit autoclean check generate-lock-entry help licenses list outdated policies upgrade-interactive".split(
19957
+ " "
19958
+ )
19959
+ );
19960
+ var RUNNER_VALUE = /* @__PURE__ */ new Set([
19961
+ "-C",
19962
+ "--dir",
19963
+ "--prefix",
19964
+ "--filter",
19965
+ "-F",
19966
+ "-w",
19967
+ "--workspace",
19968
+ "--cwd"
19969
+ ]);
19970
+ var NPM_LIFECYCLE = {
19971
+ test: "test",
19972
+ t: "test",
19973
+ tst: "test",
19974
+ start: "start",
19975
+ stop: "stop",
19976
+ restart: "restart"
19977
+ };
19978
+ function scriptName(words, scripts) {
19979
+ if (words.length < 2) return null;
19980
+ const tool = base(words[0]);
19981
+ const rest = skipFlags(words.slice(1), RUNNER_VALUE);
19982
+ const sub = rest[0];
19983
+ if (!sub) return null;
19984
+ const after = skipFlags(rest.slice(1), RUNNER_VALUE)[0] ?? null;
19985
+ if (tool === "npm") {
19986
+ if (["run", "run-script", "rum", "urn"].includes(sub)) return after;
19987
+ return NPM_LIFECYCLE[sub] ?? null;
19988
+ }
19989
+ if (tool === "pnpm") {
19990
+ if (sub === "run" || sub === "run-script") return after;
19991
+ return PNPM_BUILTINS.has(sub) ? null : sub;
19992
+ }
19993
+ if (tool === "yarn") {
19994
+ if (sub === "run") return after;
19995
+ return YARN_BUILTINS.has(sub) ? null : sub;
19996
+ }
19997
+ if (tool === "bun") {
19998
+ if (sub === "run") return after;
19999
+ return scripts && sub in scripts ? sub : null;
20000
+ }
20001
+ return null;
20002
+ }
20003
+ function label(words) {
20004
+ const text = words.join(" ");
20005
+ return text.length > MAX_LABEL ? `${text.slice(0, MAX_LABEL - 1)}\u2026` : text;
20006
+ }
20007
+ function expandCommand(command, options) {
20008
+ const maxDepth = options.maxDepth ?? MAX_DEPTH;
20009
+ const maxSegments = options.maxSegments ?? MAX_SEGMENTS;
20010
+ const segments = [];
20011
+ let scripts;
20012
+ const loadScripts = () => {
20013
+ if (scripts === void 0) {
20014
+ try {
20015
+ scripts = options.readScripts?.(options.cwd) ?? null;
20016
+ } catch {
20017
+ scripts = null;
20018
+ }
20019
+ }
20020
+ return scripts;
20021
+ };
20022
+ const visit2 = (text, via, depth, seen) => {
20023
+ for (const pipeline of splitList(text)) {
20024
+ if (segments.length >= maxSegments) return;
20025
+ const group = /^\(([\s\S]*)\)$/.exec(pipeline) ?? /^\{([\s\S]*?);?\s*\}$/.exec(pipeline);
20026
+ if (group) {
20027
+ if (depth < maxDepth) visit2(group[1], via, depth + 1, seen);
20028
+ continue;
20029
+ }
20030
+ const commands = splitPipeline(pipeline).map((part) => stripPrefixes(shellWords(part)));
20031
+ const flat = commands.map((words) => words.join(" ")).filter(Boolean).join(" | ");
20032
+ if (flat) segments.push({ text: flat, via });
20033
+ if (depth >= maxDepth) continue;
20034
+ for (const inner of substitutions(pipeline)) {
20035
+ visit2(inner, [...via, "$(\u2026)"], depth + 1, seen);
20036
+ }
20037
+ for (const words of commands) {
20038
+ const script = innerScript(words);
20039
+ if (script) {
20040
+ const wrapper = base(words[0]) === "eval" ? "eval" : `${base(words[0])} -c`;
20041
+ visit2(script, [...via, wrapper], depth + 1, seen);
20042
+ }
20043
+ const name = scriptName(words, loadScripts() ?? null);
20044
+ const all = loadScripts();
20045
+ if (!name || !all || seen.has(name) || !(name in all)) continue;
20046
+ const nextSeen = new Set(seen).add(name);
20047
+ const from = [...via, label(words)];
20048
+ for (const key of [`pre${name}`, name, `post${name}`]) {
20049
+ const body = all[key];
20050
+ if (typeof body === "string" && body.trim()) {
20051
+ visit2(body.slice(0, 2e3), from, depth + 1, nextSeen);
20052
+ }
20053
+ }
20054
+ }
20055
+ }
20056
+ };
20057
+ visit2(command, [], 0, /* @__PURE__ */ new Set());
20058
+ return segments;
20059
+ }
20060
+
19730
20061
  // ../../features/gates/shared/presets.ts
19731
20062
  var presetSchema = external_exports.object({
19732
20063
  name: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/).min(1),
@@ -19786,7 +20117,8 @@ var DEFAULT_PRESETS = [
19786
20117
  {
19787
20118
  name: "publish",
19788
20119
  patterns: [
19789
- "\\b(npm|pnpm|yarn)\\s+publish\\b",
20120
+ // Flags (and their values) may sit between the tool and "publish": pnpm --filter cli publish.
20121
+ "\\b(npm|pnpm|yarn)\\b[^;&|\\n]*\\s+publish(\\s|$)",
19790
20122
  "\\bcargo\\s+publish\\b",
19791
20123
  "\\btwine\\s+upload\\b"
19792
20124
  ],
@@ -19797,8 +20129,8 @@ var DEFAULT_PRESETS = [
19797
20129
  {
19798
20130
  name: "infra",
19799
20131
  patterns: [
19800
- "\\bterraform\\s+(apply|destroy)\\b",
19801
- "\\bpulumi\\s+(up|destroy)\\b",
20132
+ "\\bterraform\\b[^;&|\\n]*\\s+(apply|destroy)(\\s|$)",
20133
+ "\\bpulumi\\b[^;&|\\n]*\\s+(up|destroy)(\\s|$)",
19802
20134
  "\\baws\\s+.*\\bdelete-[a-z-]+\\b",
19803
20135
  "\\baws\\s+s3\\s+(rm|rb)\\b",
19804
20136
  "\\bgcloud\\s+.*\\bdelete\\b"
@@ -19806,6 +20138,21 @@ var DEFAULT_PRESETS = [
19806
20138
  unless: [DRY_RUN],
19807
20139
  quorum: 2
19808
20140
  },
20141
+ {
20142
+ // Code Tollgate can't read before it runs: fetched from the network, decoded, or built
20143
+ // from a variable at run time. A person should look before it runs.
20144
+ name: "opaque",
20145
+ patterns: [
20146
+ "\\b(curl|wget)\\b[^|;&]*\\|\\s*(sudo(\\s+-\\S+)*\\s+)?(env\\s+)?(ba|z|da|k)?sh\\b",
20147
+ "\\bbase64\\s+(-d|-D|--decode)\\b[^|;&]*\\|\\s*(sudo\\s+)?(ba|z|da|k)?sh\\b",
20148
+ "\\b(ba|z|da|k)?sh\\s+<\\(",
20149
+ "(^|[\\s;&|])(source|\\.)\\s+<\\(",
20150
+ "(^|[\\s;&|])eval\\s+[\"']?[$`]",
20151
+ "\\b(ba|z|da|k)?sh(\\s+-\\w+)*\\s+-\\w*c\\s+[\"']?[$`]"
20152
+ ],
20153
+ env: "other",
20154
+ quorum: 1
20155
+ },
19809
20156
  {
19810
20157
  name: "destructive-fs",
19811
20158
  patterns: [
@@ -19834,8 +20181,8 @@ function compile2(source) {
19834
20181
  }
19835
20182
  return regexCache.get(source) ?? null;
19836
20183
  }
19837
- function resolvePath(base, target) {
19838
- const parts = (target.startsWith("/") ? target : `${base}/${target}`).split("/");
20184
+ function resolvePath(base2, target) {
20185
+ const parts = (target.startsWith("/") ? target : `${base2}/${target}`).split("/");
19839
20186
  const out = [];
19840
20187
  for (const part of parts) {
19841
20188
  if (part === "" || part === ".") continue;
@@ -19875,6 +20222,35 @@ function matchPreset(presets, input2) {
19875
20222
  }
19876
20223
  return null;
19877
20224
  }
20225
+ function withDefaultPresets(presets) {
20226
+ const defaults = new Map(DEFAULT_PRESETS.map((preset) => [preset.name, preset]));
20227
+ const names = new Set(presets.map((preset) => preset.name));
20228
+ const merged = presets.map((preset) => {
20229
+ const builtIn = defaults.get(preset.name);
20230
+ return builtIn ? { ...preset, patterns: [.../* @__PURE__ */ new Set([...preset.patterns, ...builtIn.patterns])] } : preset;
20231
+ });
20232
+ return [...merged, ...DEFAULT_PRESETS.filter((preset) => !names.has(preset.name))];
20233
+ }
20234
+ var ENV_RANK = { prod: 2, staging: 1, other: 0 };
20235
+ function matchCommand(presets, input2) {
20236
+ const pieces = [
20237
+ { text: input2.command, via: [] },
20238
+ ...expandCommand(input2.command, { cwd: input2.cwd, readScripts: input2.readScripts }),
20239
+ ...input2.extra ?? []
20240
+ ];
20241
+ let best = null;
20242
+ for (const piece of pieces) {
20243
+ const match = matchPreset(presets, {
20244
+ command: piece.text,
20245
+ cwd: input2.cwd,
20246
+ repoRoot: input2.repoRoot
20247
+ });
20248
+ if (!match) continue;
20249
+ const better = best === null || match.quorum > best.quorum || match.quorum === best.quorum && ENV_RANK[match.env] > ENV_RANK[best.env] || match.quorum === best.quorum && match.env === best.env && best.via.length === 0 && piece.via.length > 0 && best.preset.name === match.preset.name;
20250
+ if (better) best = { ...match, segment: piece.text, via: piece.via };
20251
+ }
20252
+ return best;
20253
+ }
19878
20254
 
19879
20255
  // src/api.ts
19880
20256
  var REQUEST_TIMEOUT_MS = 5e3;
@@ -19954,6 +20330,28 @@ function findRepoRoot(cwd) {
19954
20330
  dir = parent;
19955
20331
  }
19956
20332
  }
20333
+ function readScripts(cwd, repoRoot) {
20334
+ let dir = cwd;
20335
+ for (; ; ) {
20336
+ const file2 = join(dir, "package.json");
20337
+ if (existsSync(file2)) {
20338
+ try {
20339
+ const scripts = JSON.parse(readFileSync(file2, "utf8")).scripts;
20340
+ if (!scripts || typeof scripts !== "object") return null;
20341
+ return Object.fromEntries(
20342
+ Object.entries(scripts).filter(
20343
+ (entry) => typeof entry[1] === "string"
20344
+ )
20345
+ );
20346
+ } catch {
20347
+ return null;
20348
+ }
20349
+ }
20350
+ const parent = dirname(dir);
20351
+ if (dir === repoRoot || parent === dir || !dir.startsWith(repoRoot)) return null;
20352
+ dir = parent;
20353
+ }
20354
+ }
19957
20355
  function readConfigFile(repoRoot) {
19958
20356
  const raw = JSON.parse(readFileSync(join(repoRoot, CONFIG_FILE), "utf8"));
19959
20357
  return configSchema.parse(raw);
@@ -20116,9 +20514,16 @@ var createGateSchema = external_exports.object({
20116
20514
  context: external_exports.string().max(2048).default(""),
20117
20515
  // Claude Code's own one-line description of the Bash call ("Apply pending migrations").
20118
20516
  intent: external_exports.string().max(300).nullable().optional(),
20517
+ // What a wrapper or package script really runs ("pnpm db:deploy" -> "prisma migrate deploy"),
20518
+ // redacted by the hook. The server matches it again; it can only raise a gate, never lower it.
20519
+ via: external_exports.array(
20520
+ external_exports.object({ from: external_exports.string().min(1).max(300), runs: external_exports.string().min(1).max(1e3) }).strict()
20521
+ ).max(5).optional(),
20119
20522
  redactions: external_exports.number().int().min(0).max(1e4).default(0),
20120
20523
  cmdHash: external_exports.string().regex(SHA256_RE),
20121
- sessionId: external_exports.string().max(200).optional()
20524
+ sessionId: external_exports.string().max(200).optional(),
20525
+ // How long this hook waits before parking the gate (CLI 0.2.3+), for the countdown.
20526
+ waitSec: external_exports.number().int().min(5).max(600).optional()
20122
20527
  }).strict();
20123
20528
  var consumeGrantSchema = external_exports.object({ cmdHash: external_exports.string().regex(SHA256_RE) }).strict();
20124
20529
 
@@ -20231,7 +20636,12 @@ async function decide(input2, deps) {
20231
20636
  if (!command.trim()) return { kind: "none" };
20232
20637
  const config2 = deps.loadConfig(cwd);
20233
20638
  if (!config2) return { kind: "none", note: "no .tollgate.json" };
20234
- const match = matchPreset(config2.presets, { command, cwd, repoRoot: config2.repoRoot });
20639
+ const match = matchCommand(withDefaultPresets(config2.presets), {
20640
+ command,
20641
+ cwd,
20642
+ repoRoot: config2.repoRoot,
20643
+ readScripts: deps.readScripts ? (dir) => deps.readScripts(dir, config2.repoRoot) : void 0
20644
+ });
20235
20645
  if (!match) return { kind: "none" };
20236
20646
  if (deps.env.TOLLGATE_BYPASS === "1") {
20237
20647
  deps.audit("bypass", { preset: match.preset.name, repo: config2.repo });
@@ -20257,7 +20667,12 @@ async function decide(input2, deps) {
20257
20667
  const secrets = deps.envSecrets(config2.repoRoot);
20258
20668
  const redactedCommand = redact(command, secrets);
20259
20669
  const redactedCwd = redact(cwd, secrets);
20260
- const context = redact(deps.context(command, config2.repoRoot), secrets);
20670
+ const context = redact(deps.context(`${command}
20671
+ ${match.segment}`, config2.repoRoot), secrets);
20672
+ const via = match.via.length > 0 ? {
20673
+ from: redact(match.via.join(" \u2192 "), secrets),
20674
+ runs: redact(match.segment, secrets)
20675
+ } : null;
20261
20676
  const branch = deps.gitBranch(config2.repoRoot);
20262
20677
  const description = input2.tool_input?.description;
20263
20678
  const intent = typeof description === "string" && description.trim() ? redact(description.trim(), secrets) : null;
@@ -20270,9 +20685,11 @@ async function decide(input2, deps) {
20270
20685
  branch: branch ? truncate(redact(branch, secrets).text, 200) : null,
20271
20686
  context: truncate(context.text, 2048),
20272
20687
  intent: intent ? truncate(intent.text, 300) : null,
20273
- redactions: redactedCommand.count + redactedCwd.count + context.count + (intent?.count ?? 0),
20688
+ via: via ? [{ from: truncate(via.from.text, 300), runs: truncate(via.runs.text, 1e3) }] : void 0,
20689
+ redactions: redactedCommand.count + redactedCwd.count + context.count + (intent?.count ?? 0) + (via ? via.from.count + via.runs.count : 0),
20274
20690
  cmdHash,
20275
- sessionId: input2.session_id
20691
+ sessionId: input2.session_id,
20692
+ waitSec: Math.round((deadline - started) / 1e3)
20276
20693
  });
20277
20694
  deps.progress(
20278
20695
  `Tollgate: waiting on your team \xB7 TG-${gate.code} (0/${gate.quorum}) \xB7 ${gate.url}`
@@ -20388,7 +20805,7 @@ function writeRepoFiles(input2) {
20388
20805
  }
20389
20806
 
20390
20807
  // src/main.ts
20391
- var HELP = `tollgate ${"0.2.1"}: your team approves risky agent commands before they run
20808
+ var HELP = `tollgate ${"0.2.3"}: your team approves risky agent commands before they run
20392
20809
 
20393
20810
  Usage:
20394
20811
  npx tollgate-cli init --token <tg_\u2026> [--server <url>] [--repo <owner/name>]
@@ -20405,6 +20822,7 @@ function hookDeps() {
20405
20822
  envSecrets,
20406
20823
  gitBranch,
20407
20824
  context: buildContext,
20825
+ readScripts,
20408
20826
  env: process.env,
20409
20827
  now: () => Date.now(),
20410
20828
  sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
@@ -20572,7 +20990,7 @@ async function main() {
20572
20990
  return runUnlink();
20573
20991
  case "--version":
20574
20992
  case "-v":
20575
- process.stdout.write(`${"0.2.1"}
20993
+ process.stdout.write(`${"0.2.3"}
20576
20994
  `);
20577
20995
  return;
20578
20996
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tollgate-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Your team approves risky Claude Code commands before they run.",
5
5
  "license": "MIT",
6
6
  "type": "module",