Harden jq safe-bin semantics (#102032)

This commit is contained in:
Pavan Kumar Gondhi
2026-07-08 15:43:37 +05:30
committed by GitHub
parent 4ae8d735bf
commit 5f04dc97e6
10 changed files with 205 additions and 89 deletions
+6 -7
View File
@@ -61,12 +61,11 @@ Denied flags by safe-bin profile:
Safe bins also force argv tokens to be treated as **literal text** at execution
time (no globbing and no `$VARS` expansion) for stdin-only segments, so
patterns like `*` or `$HOME/...` cannot be used to smuggle file reads. `awk`
and `sed` are always denied as safe bins (their semantics cannot be validated
to stdin-only); `jq` can be opted in, but OpenClaw still rejects `env`-style
filters (for example `jq env` or `jq -n env`) in safe-bin mode so `jq` cannot
dump the host process environment without an explicit allowlist path or
approval prompt.
patterns like `*` or `$HOME/...` cannot be used to smuggle file reads. `awk`,
`sed`, and `jq` are always denied as safe bins because their semantics cannot be
validated to stdin-only: `jq` can read environment data and load jq code from
modules or startup files. Use an explicit allowlist entry or approval prompt for
those tools instead of `safeBins`.
### Trusted binary directories
@@ -131,7 +130,7 @@ Custom profile example:
{
tools: {
exec: {
safeBins: ["jq", "myfilter"],
safeBins: ["myfilter"],
safeBinProfiles: {
myfilter: {
minPositional: 0,
+1 -1
View File
@@ -189,7 +189,7 @@ Use the two controls for different jobs:
Do not treat `safeBins` as a generic allowlist, and do not add interpreter/runtime binaries (for example `python3`, `node`, `ruby`, `bash`). If you need those, use explicit allowlist entries and keep approval prompts enabled.
`openclaw security audit` warns when interpreter/runtime `safeBins` entries are missing explicit profiles, and `openclaw doctor --fix` can scaffold missing custom `safeBinProfiles` entries. `openclaw security audit` and `openclaw doctor` also warn when you explicitly add broad-behavior bins such as `jq` back into `safeBins` (`jq` supports broad programs and builtins, so prefer explicit allowlist entries or approval-gated runs instead). If you explicitly allowlist interpreters, enable `tools.exec.strictInlineEval` so inline code-eval forms still require reviewer or explicit approval.
`openclaw security audit` warns when interpreter/runtime `safeBins` entries are missing explicit profiles, and `openclaw doctor --fix` can scaffold missing custom `safeBinProfiles` entries. `openclaw security audit` and `openclaw doctor` also warn when you explicitly add broad-behavior bins such as `jq` back into `safeBins` (`jq` can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead). `jq` is denied as a safe bin even when it is explicitly listed. If you explicitly allowlist interpreters, enable `tools.exec.strictInlineEval` so inline code-eval forms still require reviewer or explicit approval.
For full policy details and examples, see [Exec approvals](/tools/exec-approvals-advanced#safe-bins-stdin-only) and [Safe bins versus allowlist](/tools/exec-approvals-advanced#safe-bins-versus-allowlist).
@@ -36,7 +36,7 @@ describe("doctor exec safe bin helpers", () => {
bin: "jq",
kind: "riskySemantics",
warning:
"jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
},
]);
});
@@ -50,7 +50,13 @@ describe("doctor exec safe bin helpers", () => {
bin: "jq",
kind: "riskySemantics",
warning:
"jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
},
{
scopePath: "tools.exec",
bin: "myfilter",
kind: "missingProfile",
isInterpreter: false,
},
],
doctorFixCommand: "openclaw doctor --fix",
@@ -58,28 +64,93 @@ describe("doctor exec safe bin helpers", () => {
expect(warnings).toEqual([
"- tools.exec.safeBins includes interpreter/runtime 'node' without profile.",
"- agents.list.runner.tools.exec.safeBins includes 'jq': jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"- tools.exec.safeBins entry 'myfilter' is missing safeBinProfiles.myfilter.",
"- agents.list.runner.tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
'- Run "openclaw doctor --fix" to scaffold missing custom safeBinProfiles entries.',
]);
});
it("scaffolds custom safeBin profiles but warns on interpreters", () => {
it("omits doctor fix hint when no custom safeBin profiles can be scaffolded", () => {
const warnings = collectExecSafeBinCoverageWarnings({
hits: [
{
scopePath: "tools.exec",
bin: "jq",
kind: "riskySemantics",
warning:
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
},
],
doctorFixCommand: "openclaw doctor --fix",
});
expect(warnings).toEqual([
"- tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
]);
});
it("scaffolds custom safeBin profiles but warns on interpreters and risky bins", () => {
const result = maybeRepairExecSafeBinProfiles({
tools: {
exec: {
safeBins: ["node", "jq"],
safeBins: ["node", "jq", "myfilter"],
},
},
} as OpenClawConfig);
expect(result.changes).toEqual([
"- tools.exec.safeBinProfiles.jq: added scaffold profile {} (review and tighten flags/positionals).",
"- tools.exec.safeBinProfiles.myfilter: added scaffold profile {} (review and tighten flags/positionals).",
]);
expect(result.warnings).toEqual([
"- tools.exec.safeBins includes 'jq': jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"- tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"- tools.exec.safeBins includes interpreter/runtime 'node' without profile; remove it from safeBins or use explicit allowlist entries.",
]);
expect(result.config.tools?.exec?.safeBinProfiles).toEqual({ jq: {} });
expect(result.config.tools?.exec?.safeBinProfiles).toEqual({ myfilter: {} });
});
it("does not scaffold normalized risky safeBins from path-like entries", () => {
const hits = scanExecSafeBinCoverage({
tools: {
exec: {
safeBins: ["/usr/local/bin/jq", "sed.exe", "myfilter"],
},
},
} as OpenClawConfig);
expect(hits).toEqual([
{ scopePath: "tools.exec", bin: "myfilter", kind: "missingProfile", isInterpreter: false },
{
scopePath: "tools.exec",
bin: "jq",
kind: "riskySemantics",
warning:
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
},
{
scopePath: "tools.exec",
bin: "sed",
kind: "riskySemantics",
warning:
"sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
},
]);
const result = maybeRepairExecSafeBinProfiles({
tools: {
exec: {
safeBins: ["/usr/local/bin/jq", "sed.exe", "myfilter"],
},
},
} as OpenClawConfig);
expect(result.changes).toEqual([
"- tools.exec.safeBinProfiles.myfilter: added scaffold profile {} (review and tighten flags/positionals).",
]);
expect(result.warnings).toEqual([
"- tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"- tools.exec.safeBins includes 'sed': sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
]);
expect(result.config.tools?.exec?.safeBinProfiles).toEqual({ myfilter: {} });
});
it("warns on awk-family safeBins instead of scaffolding them", () => {
@@ -95,10 +166,8 @@ describe("doctor exec safe bin helpers", () => {
expect(result.warnings).toEqual([
"- tools.exec.safeBins includes 'awk': awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"- tools.exec.safeBins includes 'sed': sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"- tools.exec.safeBins includes interpreter/runtime 'awk' without profile; remove it from safeBins or use explicit allowlist entries.",
"- tools.exec.safeBins includes interpreter/runtime 'sed' without profile; remove it from safeBins or use explicit allowlist entries.",
]);
expect(result.config.tools?.exec?.safeBinProfiles).toStrictEqual({});
expect(result.config.tools?.exec?.safeBinProfiles).toBeUndefined();
});
it("warns on busybox/toybox safeBins instead of scaffolding them", () => {
+21 -7
View File
@@ -10,7 +10,10 @@ import {
listInterpreterLikeSafeBins,
resolveMergedSafeBinProfileFixtures,
} from "../../../infra/exec-safe-bin-runtime-policy.js";
import { listRiskyConfiguredSafeBins } from "../../../infra/exec-safe-bin-semantics.js";
import {
listRiskyConfiguredSafeBins,
normalizeSafeBinName,
} from "../../../infra/exec-safe-bin-semantics.js";
import { getTrustedSafeBinDirs, isTrustedSafeBinPath } from "../../../infra/exec-safe-bin-trust.js";
import { asObjectRecord } from "./object.js";
@@ -103,10 +106,15 @@ export function scanExecSafeBinCoverage(cfg: OpenClawConfig): ExecSafeBinCoverag
const hits: ExecSafeBinCoverageHit[] = [];
for (const scope of collectExecSafeBinScopes(cfg)) {
const interpreterBins = new Set(listInterpreterLikeSafeBins(scope.safeBins));
const riskyHits = listRiskyConfiguredSafeBins(scope.safeBins);
const riskyBins = new Set(riskyHits.map((hit) => hit.bin));
for (const bin of scope.safeBins) {
if (scope.mergedProfiles[bin]) {
continue;
}
if (riskyBins.has(normalizeSafeBinName(bin))) {
continue;
}
hits.push({
scopePath: scope.scopePath,
bin,
@@ -114,7 +122,7 @@ export function scanExecSafeBinCoverage(cfg: OpenClawConfig): ExecSafeBinCoverag
isInterpreter: interpreterBins.has(bin),
});
}
for (const hit of listRiskyConfiguredSafeBins(scope.safeBins)) {
for (const hit of riskyHits) {
hits.push({
scopePath: scope.scopePath,
bin: hit.bin,
@@ -205,9 +213,11 @@ export function collectExecSafeBinCoverageWarnings(params: {
);
}
}
lines.push(
`- Run "${params.doctorFixCommand}" to scaffold missing custom safeBinProfiles entries.`,
);
if (customHits.length > 0) {
lines.push(
`- Run "${params.doctorFixCommand}" to scaffold missing custom safeBinProfiles entries.`,
);
}
return lines;
}
@@ -245,10 +255,14 @@ export function maybeRepairExecSafeBinProfiles(cfg: OpenClawConfig): {
for (const scope of collectExecSafeBinScopes(next)) {
const interpreterBins = new Set(listInterpreterLikeSafeBins(scope.safeBins));
for (const hit of listRiskyConfiguredSafeBins(scope.safeBins)) {
const riskyHits = listRiskyConfiguredSafeBins(scope.safeBins);
const riskyBins = new Set(riskyHits.map((hit) => hit.bin));
for (const hit of riskyHits) {
warnings.push(`- ${scope.scopePath}.safeBins includes '${hit.bin}': ${hit.warning}`);
}
const missingBins = scope.safeBins.filter((bin) => !scope.mergedProfiles[bin]);
const missingBins = scope.safeBins.filter(
(bin) => !scope.mergedProfiles[bin] && !riskyBins.has(normalizeSafeBinName(bin)),
);
if (missingBins.length === 0) {
continue;
}
+5 -5
View File
@@ -114,9 +114,9 @@ describe("exec approvals node host allowlist check", () => {
it("satisfies via safeBins even when not in allowlist", () => {
const resolution = {
rawExecutable: "jq",
resolvedPath: "/usr/bin/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/usr/bin/head",
executableName: "head",
};
// Not in allowlist
const entries: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/python3" }];
@@ -125,9 +125,9 @@ describe("exec approvals node host allowlist check", () => {
// But is a safe bin with non-file args
const safe = isSafeBinUsage({
argv: ["jq", ".foo"],
argv: ["head", "-n", "1"],
resolution,
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
});
// Safe bins are disabled on Windows (PowerShell parsing/expansion differences).
if (process.platform === "win32") {
+59 -35
View File
@@ -177,10 +177,10 @@ describe("exec approvals safe bins", () => {
const cases: SafeBinCase[] = [
{
name: "allows safe bins with non-path args",
name: "blocks jq safe bins even with non-path args",
argv: ["jq", ".foo"],
resolvedPath: "/usr/bin/jq",
expected: true,
expected: false,
},
{
name: "blocks jq env builtin even when jq is explicitly opted in",
@@ -200,6 +200,30 @@ describe("exec approvals safe bins", () => {
resolvedPath: "/usr/bin/jq",
expected: false,
},
{
name: "blocks jq include directives even when jq is explicitly opted in",
argv: ["jq", 'include "envdump"; envdump'],
resolvedPath: "/usr/bin/jq",
expected: false,
},
{
name: "blocks jq import directives even when jq is explicitly opted in",
argv: ["jq", 'import "envdump" as envdump; envdump::read'],
resolvedPath: "/usr/bin/jq",
expected: false,
},
{
name: "blocks jq field names that match directive keywords",
argv: ["jq", ".include + .import"],
resolvedPath: "/usr/bin/jq",
expected: false,
},
{
name: "blocks jq object keys that match directive keywords",
argv: ["jq", "{include: .foo, import : .bar}"],
resolvedPath: "/usr/bin/jq",
expected: false,
},
{
name: "blocks awk scripts even when awk is explicitly profiled",
argv: ["awk", 'BEGIN { system("id") }'],
@@ -322,13 +346,13 @@ describe("exec approvals safe bins", () => {
return;
}
const ok = isSafeBinUsage({
argv: ["jq", ".foo"],
argv: ["head", "-n", "1"],
resolution: {
rawExecutable: "jq",
resolvedPath: "/custom/bin/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/custom/bin/head",
executableName: "head",
},
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
trustedSafeBinDirs: new Set(["/custom/bin"]),
});
expect(ok).toBe(true);
@@ -339,35 +363,35 @@ describe("exec approvals safe bins", () => {
return;
}
const resolution = {
rawExecutable: "jq",
resolvedPath: "/opt/homebrew/bin/jq",
resolvedRealPath: "/opt/homebrew/Cellar/jq/1.7.1/bin/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/opt/homebrew/bin/head",
resolvedRealPath: "/opt/homebrew/Cellar/coreutils/9.5/bin/head",
executableName: "head",
};
expect(
isSafeBinUsage({
argv: ["jq", ".foo"],
argv: ["head", "-n", "1"],
resolution,
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
trustedSafeBinDirs: new Set(["/opt/homebrew/bin"]),
}),
).toBe(false);
expect(
isSafeBinUsage({
argv: ["jq", ".foo"],
argv: ["head", "-n", "1"],
resolution,
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
trustedSafeBinDirs: getTrustedSafeBinDirs({
extraDirs: ["/opt/homebrew/Cellar/jq/1.7.1/bin"],
extraDirs: ["/opt/homebrew/Cellar/coreutils/9.5/bin"],
refresh: true,
}),
}),
).toBe(true);
expect(
isSafeBinUsage({
argv: ["jq", ".foo"],
argv: ["head", "-n", "1"],
resolution,
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
trustedSafeBinDirs: new Set(["/tmp/other-bin"]),
}),
).toBe(false);
@@ -375,13 +399,13 @@ describe("exec approvals safe bins", () => {
it("supports injected platform for deterministic safe-bin checks", () => {
const ok = isSafeBinUsage({
argv: ["jq", ".foo"],
argv: ["head", "-n", "1"],
resolution: {
rawExecutable: "jq",
resolvedPath: "/usr/bin/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/usr/bin/head",
executableName: "head",
},
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
platform: "win32",
});
expect(ok).toBe(false);
@@ -392,13 +416,13 @@ describe("exec approvals safe bins", () => {
return;
}
const baseParams = {
argv: ["jq", ".foo"],
argv: ["head", "-n", "1"],
resolution: {
rawExecutable: "jq",
resolvedPath: "/tmp/custom/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/tmp/custom/head",
executableName: "head",
},
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
};
expect(
isSafeBinUsage({
@@ -527,13 +551,13 @@ describe("exec approvals safe bins", () => {
ok: true as const,
segments: [
{
raw: "jq .foo",
argv: ["jq", ".foo"],
raw: "head -n 1",
argv: ["head", "-n", "1"],
resolution: makeMockCommandResolution({
execution: makeMockExecutableResolution({
rawExecutable: "jq",
resolvedPath: "/custom/bin/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/custom/bin/head",
executableName: "head",
}),
}),
},
@@ -542,7 +566,7 @@ describe("exec approvals safe bins", () => {
const denied = evaluateExecAllowlist({
analysis,
allowlist: [],
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
trustedSafeBinDirs: new Set(["/usr/bin"]),
cwd: "/tmp",
});
@@ -551,7 +575,7 @@ describe("exec approvals safe bins", () => {
const allowed = evaluateExecAllowlist({
analysis,
allowlist: [],
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
trustedSafeBinDirs: new Set(["/custom/bin"]),
cwd: "/tmp",
});
+12 -12
View File
@@ -67,13 +67,13 @@ describe("exec approvals allowlist evaluation", () => {
ok: true,
segments: [
{
raw: "jq .foo",
argv: ["jq", ".foo"],
raw: "head -n 1",
argv: ["head", "-n", "1"],
resolution: makeMockCommandResolution({
execution: makeMockExecutableResolution({
rawExecutable: "jq",
resolvedPath: "/usr/bin/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/usr/bin/head",
executableName: "head",
}),
}),
},
@@ -82,7 +82,7 @@ describe("exec approvals allowlist evaluation", () => {
const result = evaluateExecAllowlist({
analysis,
allowlist: [],
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
cwd: "/tmp",
});
// Safe bins are disabled on Windows (PowerShell parsing/expansion differences).
@@ -238,13 +238,13 @@ describe("exec approvals allowlist evaluation", () => {
}),
};
const safeBinSegment = {
raw: "jq .foo",
argv: ["jq", ".foo"],
raw: "head -n 1",
argv: ["head", "-n", "1"],
resolution: makeMockCommandResolution({
execution: makeMockExecutableResolution({
rawExecutable: "jq",
resolvedPath: "/usr/bin/jq",
executableName: "jq",
rawExecutable: "head",
resolvedPath: "/usr/bin/head",
executableName: "head",
}),
}),
};
@@ -256,7 +256,7 @@ describe("exec approvals allowlist evaluation", () => {
const result = evaluateExecAllowlist({
analysis,
allowlist: [{ pattern: "/usr/bin/tool" }],
safeBins: normalizeSafeBins(["jq"]),
safeBins: normalizeSafeBins(["head"]),
cwd: "/tmp",
});
if (process.platform === "win32") {
+3 -3
View File
@@ -78,9 +78,9 @@ describe("exec safe bin policy grep", () => {
describe("exec safe bin policy jq", () => {
const jqProfile = SAFE_BIN_PROFILES.jq;
it("allows normal jq field filters", () => {
expect(validateSafeBinArgv([".foo"], jqProfile, { binName: "jq" })).toBe(true);
expect(validateSafeBinArgv([".env"], jqProfile, { binName: "jq" })).toBe(true);
it("blocks normal jq field filters in safe-bin mode", () => {
expect(validateSafeBinArgv([".foo"], jqProfile, { binName: "jq" })).toBe(false);
expect(validateSafeBinArgv([".env"], jqProfile, { binName: "jq" })).toBe(false);
});
it("blocks jq env builtin filters in safe-bin mode", () => {
+16 -1
View File
@@ -6,6 +6,21 @@ import {
} from "./exec-safe-bin-semantics.js";
describe("exec safe-bin semantics", () => {
it("rejects jq programs even when configured via path-like entries", () => {
expect(
validateSafeBinSemantics({
binName: "jq",
positional: [".foo"],
}),
).toBe(false);
expect(
validateSafeBinSemantics({
binName: "/usr/local/bin/jq",
positional: ["{include: .foo, import : .bar}"],
}),
).toBe(false);
});
it("rejects awk and sed variants even when configured via path-like entries", () => {
expect(
validateSafeBinSemantics({
@@ -64,7 +79,7 @@ describe("exec safe-bin semantics", () => {
{
bin: "jq",
warning:
"jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
},
{
bin: "mawk",
+2 -7
View File
@@ -11,22 +11,17 @@ type SafeBinSemanticRule = {
configWarning?: string;
};
const JQ_ENV_FILTER_PATTERN = /(^|[^.$A-Za-z0-9_])env([^A-Za-z0-9_]|$)/;
const JQ_ENV_VARIABLE_PATTERN = /\$ENV\b/;
const ALWAYS_DENY_SAFE_BIN_SEMANTICS = () => false;
const UNSAFE_SAFE_BIN_WARNINGS = {
awk: "awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
jq: "jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
jq: "jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
sed: "sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
} as const;
const SAFE_BIN_SEMANTIC_RULES: Readonly<Record<string, SafeBinSemanticRule>> = {
jq: {
validate: ({ positional }) =>
!positional.some(
(token) => JQ_ENV_FILTER_PATTERN.test(token) || JQ_ENV_VARIABLE_PATTERN.test(token),
),
validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS,
configWarning: UNSAFE_SAFE_BIN_WARNINGS.jq,
},
awk: {