Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 207 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

470 changes: 152 additions & 318 deletions dist/bin/cc-safety-net.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions dist/bin/hooks/common.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export declare function readHookInput<T>(outputDeny: (reason: string) => void): Promise<T | null>;
export declare function parseHookJson<T>(inputText: string, outputDeny: (reason: string) => void, strictReason: string): T | null;
export declare function handleBlockedHookCommand(command: string, cwd: string, sessionId: string | undefined, outputDeny: (reason: string, command?: string, segment?: string) => void): void;
15 changes: 15 additions & 0 deletions dist/core/analyze/child-command.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface ChildCommandContext {
cwd: string | undefined;
envAssignments?: ReadonlyMap<string, string>;
}
export declare function normalizeChildCommand(tokens: readonly string[], context: ChildCommandContext): {
tokens: string[];
cwd: string | undefined;
wrapperCwd: string | null | undefined;
envAssignments: Map<string, string>;
head: string;
};
export declare function collectCommandTemplate(tokens: readonly string[], start: number): {
markerIndex: number;
templateTokens: string[];
};
1 change: 1 addition & 0 deletions dist/core/worktree.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export declare function isLinkedWorktree(cwd: string): boolean;
/** @internal Exported for testing */
export declare function normalizePathForComparison(path: string): string;
declare function parseGitConfigValue(value: string): string;
export declare function findDotGitInAncestors(cwd: string): string | null;
/** @internal Exported for testing */
export { parseGitConfigValue as _parseGitConfigValue };
166 changes: 73 additions & 93 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -726,12 +726,14 @@ function isDirectory(path) {
}
}
function findDotGit(cwd) {
let current;
try {
current = realpathSync2(cwd);
return findDotGitInAncestors(realpathSync2(cwd));
} catch {
return null;
}
}
function findDotGitInAncestors(cwd) {
let current = cwd;
while (true) {
const dotGitPath = join(current, ".git");
if (existsSync(dotGitPath)) {
Expand Down Expand Up @@ -1831,6 +1833,38 @@ function containsDangerousCode(code) {
return false;
}

// src/core/analyze/child-command.ts
function normalizeChildCommand(tokens, context) {
const wrapperInfo = stripWrappersWithInfo([...tokens], context.cwd);
const envAssignments = new Map(context.envAssignments ?? []);
for (const [k, v] of wrapperInfo.envAssignments) {
envAssignments.set(k, v);
}
const childTokens = getBasename(wrapperInfo.tokens[0] ?? "").toLowerCase() === "busybox" && wrapperInfo.tokens.length > 1 ? wrapperInfo.tokens.slice(1) : wrapperInfo.tokens;
return {
tokens: childTokens,
cwd: wrapperInfo.cwd === null ? undefined : wrapperInfo.cwd ?? context.cwd,
wrapperCwd: wrapperInfo.cwd,
envAssignments,
head: getBasename(childTokens[0] ?? "").toLowerCase()
};
}
function collectCommandTemplate(tokens, start) {
const templateTokens = [];
let i = start;
while (i < tokens.length) {
const token = tokens[i];
if (token === undefined || token === ":::")
break;
templateTokens.push(token);
i++;
}
return {
markerIndex: i < tokens.length && tokens[i] === ":::" ? i : -1,
templateTokens
};
}

// src/core/analyze/shell-wrappers.ts
function extractDashCArg(tokens) {
for (let i = 1;i < tokens.length; i++) {
Expand Down Expand Up @@ -2352,7 +2386,7 @@ function isGitConfigUnsetError(error) {
return typeof error === "object" && error !== null && "status" in error && error.status === 1;
}
function getLocalGitConfigPaths(cwd) {
const dotGitPath = findDotGitPath(cwd);
const dotGitPath = findDotGitInAncestors(cwd);
if (dotGitPath === null) {
return null;
}
Expand All @@ -2366,20 +2400,6 @@ function getLocalGitConfigPaths(cwd) {
}
return [join2(commonDir, "config"), join2(gitDir, "config.worktree")];
}
function findDotGitPath(cwd) {
let current = cwd;
while (true) {
const dotGitPath = join2(current, ".git");
if (existsSync2(dotGitPath)) {
return dotGitPath;
}
const parent = dirname3(current);
if (parent === current) {
return null;
}
current = parent;
}
}
function resolveGitDirFromDotGit(dotGitPath) {
try {
const content = readFileSync2(dotGitPath, "utf-8");
Expand Down Expand Up @@ -2821,20 +2841,10 @@ function analyzeParallel(tokens, context) {
}
return null;
}
const childWrapperInfo = stripWrappersWithInfo([...template], context.cwd);
let childTokens = childWrapperInfo.tokens;
const childEnvAssignments = new Map(context.envAssignments ?? []);
for (const [k, v] of childWrapperInfo.envAssignments) {
childEnvAssignments.set(k, v);
}
const childCwd = childWrapperInfo.cwd === null ? undefined : childWrapperInfo.cwd ?? context.cwd;
const nestedOverrides = buildNestedOverrides(childEnvAssignments, childWrapperInfo.cwd, runsRemotely || hasDynamicStdinPlaceholder);
let head = getBasename(childTokens[0] ?? "").toLowerCase();
if (head === "busybox" && childTokens.length > 1) {
childTokens = childTokens.slice(1);
head = getBasename(childTokens[0] ?? "").toLowerCase();
}
if (SHELL_WRAPPERS.has(head)) {
const childCommand = normalizeChildCommand(template, context);
const childTokens = childCommand.tokens;
const nestedOverrides = buildNestedOverrides(childCommand.envAssignments, childCommand.wrapperCwd, runsRemotely || hasDynamicStdinPlaceholder);
if (SHELL_WRAPPERS.has(childCommand.head)) {
const dashCArg = extractDashCArg(childTokens);
if (dashCArg) {
if (isOnlyParallelPlaceholder(dashCArg)) {
Expand Down Expand Up @@ -2874,12 +2884,12 @@ function analyzeParallel(tokens, context) {
}
return null;
}
if (head === "rm" && hasRecursiveForceFlags(childTokens)) {
if (childCommand.head === "rm" && hasRecursiveForceFlags(childTokens)) {
if (hasPlaceholder && args.length > 0) {
for (const arg of args) {
const expandedTokens = childTokens.map((t) => t.replace(/{}/g, arg));
const rmResult = analyzeRm(expandedTokens, {
cwd: childCwd,
cwd: childCommand.cwd,
originalCwd: context.originalCwd,
paranoid: context.paranoidRm,
allowTmpdirVar: context.allowTmpdirVar
Expand All @@ -2893,7 +2903,7 @@ function analyzeParallel(tokens, context) {
if (args.length > 0) {
const expandedTokens = [...childTokens, args[0] ?? ""];
const rmResult = analyzeRm(expandedTokens, {
cwd: childCwd,
cwd: childCommand.cwd,
originalCwd: context.originalCwd,
paranoid: context.paranoidRm,
allowTmpdirVar: context.allowTmpdirVar
Expand All @@ -2905,19 +2915,19 @@ function analyzeParallel(tokens, context) {
}
return REASON_PARALLEL_RM;
}
if (head === "find") {
if (childCommand.head === "find") {
const findResult = analyzeFind(childTokens);
if (findResult) {
return findResult;
}
}
if (head === "git") {
if (childCommand.head === "git") {
const gitTokenSets = hasPlaceholder && args.length > 0 ? args.map((arg) => childTokens.map((token) => replaceParallelPlaceholder(token, arg))) : !hasPlaceholder && args.length > 0 ? args.map((arg) => [...childTokens, arg]) : [childTokens];
const dynamicGitArgs = usesStdin || hasPlaceholder;
for (const gitTokens of gitTokenSets) {
const gitResult = analyzeGit(gitTokens, {
cwd: childCwd,
envAssignments: childEnvAssignments,
cwd: childCommand.cwd,
envAssignments: childCommand.envAssignments,
worktreeMode: runsRemotely || dynamicGitArgs ? false : context.worktreeMode
});
if (gitResult) {
Expand Down Expand Up @@ -2987,17 +2997,9 @@ function parseParallelCommand(tokens) {
break;
}
if (token === "--") {
i++;
while (i < tokens.length) {
const token2 = tokens[i];
if (token2 === undefined || token2 === ":::")
break;
templateTokens.push(token2);
i++;
}
if (i < tokens.length && tokens[i] === ":::") {
markerIndex = i;
}
const template = collectCommandTemplate(tokens, i + 1);
templateTokens.push(...template.templateTokens);
markerIndex = template.markerIndex;
break;
}
if (token.startsWith("-")) {
Expand Down Expand Up @@ -3034,16 +3036,9 @@ function parseParallelCommand(tokens) {
}
i++;
} else {
while (i < tokens.length) {
const token2 = tokens[i];
if (token2 === undefined || token2 === ":::")
break;
templateTokens.push(token2);
i++;
}
if (i < tokens.length && tokens[i] === ":::") {
markerIndex = i;
}
const template = collectCommandTemplate(tokens, i);
templateTokens.push(...template.templateTokens);
markerIndex = template.markerIndex;
break;
}
}
Expand Down Expand Up @@ -3101,27 +3096,17 @@ var REASON_XARGS_SHELL = "xargs with shell -c can execute arbitrary commands fro
var XARGS_APPENDED_INPUT = "__CC_SAFETY_NET_XARGS_INPUT__";
function analyzeXargs(tokens, context) {
const { childTokens: rawChildTokens, replacementToken } = extractXargsChildCommandWithInfo(tokens);
const childWrapperInfo = stripWrappersWithInfo(rawChildTokens, context.cwd);
let childTokens = childWrapperInfo.tokens;
const childEnvAssignments = new Map(context.envAssignments ?? []);
for (const [k, v] of childWrapperInfo.envAssignments) {
childEnvAssignments.set(k, v);
}
const childCwd = childWrapperInfo.cwd === null ? undefined : childWrapperInfo.cwd ?? context.cwd;
const childCommand = normalizeChildCommand(rawChildTokens, context);
const childTokens = childCommand.tokens;
if (childTokens.length === 0) {
return null;
}
let head = getBasename(childTokens[0] ?? "").toLowerCase();
if (head === "busybox" && childTokens.length > 1) {
childTokens = childTokens.slice(1);
head = getBasename(childTokens[0] ?? "").toLowerCase();
}
if (SHELL_WRAPPERS.has(head)) {
if (SHELL_WRAPPERS.has(childCommand.head)) {
return REASON_XARGS_SHELL;
}
if (head === "rm" && hasRecursiveForceFlags(childTokens)) {
if (childCommand.head === "rm" && hasRecursiveForceFlags(childTokens)) {
const rmResult = analyzeRm(childTokens, {
cwd: childCwd,
cwd: childCommand.cwd,
originalCwd: context.originalCwd,
paranoid: context.paranoidRm,
allowTmpdirVar: context.allowTmpdirVar
Expand All @@ -3131,18 +3116,18 @@ function analyzeXargs(tokens, context) {
}
return REASON_XARGS_RM;
}
if (head === "find") {
if (childCommand.head === "find") {
const findResult = analyzeFind(childTokens);
if (findResult) {
return findResult;
}
}
if (head === "git") {
if (childCommand.head === "git") {
const gitTokens = replacementToken === null ? [...childTokens, XARGS_APPENDED_INPUT] : childTokens;
const hasDynamicReplacement = replacementToken !== null && childTokens.some((token) => token.includes(replacementToken));
const hasDynamicReplacement = replacementToken !== null && (childTokens.some((token) => token.includes(replacementToken)) || Array.from(childCommand.envAssignments.values()).some((value) => value.includes(replacementToken)));
const gitResult = analyzeGit(gitTokens, {
cwd: childCwd,
envAssignments: childEnvAssignments,
cwd: childCommand.cwd,
envAssignments: childCommand.envAssignments,
worktreeMode: replacementToken === null || hasDynamicReplacement ? false : context.worktreeMode
});
if (gitResult) {
Expand Down Expand Up @@ -3761,13 +3746,7 @@ function addExportedGitContextEnvAssignment(state, token) {
return;
}
if (isTrackedGitEnvName2(token)) {
state.exportedNames.add(token);
const value = state.shellAssignments.get(token);
if (value !== undefined) {
setEffectiveGitContextAssignment(state, { name: token, value });
} else {
setEffectiveGitContextAssignment(state, { name: token, value: "" });
}
exportTrackedGitContextEnvName(state, token);
}
}
function addTypesetGitContextEnvAssignment(state, token, exports, readonlyLeadingAssignments) {
Expand All @@ -3789,15 +3768,16 @@ function addTypesetGitContextEnvAssignment(state, token, exports, readonlyLeadin
return;
}
if (exports && isTrackedGitEnvName2(token)) {
state.exportedNames.add(token);
const value = state.shellAssignments.get(token);
if (value !== undefined) {
setEffectiveGitContextAssignment(state, { name: token, value });
} else {
setEffectiveGitContextAssignment(state, { name: token, value: "" });
}
exportTrackedGitContextEnvName(state, token);
}
}
function exportTrackedGitContextEnvName(state, name) {
state.exportedNames.add(name);
setEffectiveGitContextAssignment(state, {
name,
value: state.shellAssignments.get(name) ?? ""
});
}
function getExportOperandsStart(tokens, commandIndex) {
let i = commandIndex + 1;
while (i < tokens.length) {
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
"build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly --declaration",
"build:schema": "bun run scripts/build-schema.ts",
"clean": "rm -rf dist",
"check": "bun run lint && bun run typecheck && bun run knip && bun run sg:scan && AGENT=1 bun test --coverage",
"check:ci": "bun run lint:ci && bun run typecheck && bun run knip && bun run sg:scan && AGENT=1 bun test --coverage --coverage-reporter=lcov",
"check": "bun run lint && bun run typecheck && bun run knip && bun run check-duplicates && bun run sg:scan && AGENT=1 bun test --coverage",
"check:ci": "bun run lint:ci && bun run typecheck && bun run knip && bun run check-duplicates && bun run sg:scan && AGENT=1 bun test --coverage --coverage-reporter=lcov",
"lint": "biome check --write",
"lint:ci": "biome ci .",
"typecheck": "tsc --noEmit",
"knip": "knip --production",
"check-duplicates": "bunx jscpd src tests --exitCode 1 --reporters ai",
"sg:scan": "ast-grep scan",
"test": "bun test",
"publish:dry-run": "bun run scripts/publish.ts --dry-run",
Expand Down Expand Up @@ -49,6 +50,7 @@
"@types/bun": "latest",
"@types/shell-quote": "^1.7.5",
"husky": "^9.1.7",
"jscpd": "^4.0.9",
"knip": "^5.79.0",
"lint-staged": "^16.2.7",
"zod": "^4.3.5"
Expand Down
Loading
Loading