Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
80ea2a0
chore: apply issue 10197 fix
SLP-DEV1 Aug 26, 2026
85b86e1
fix(core): preserve env prefixes in Bash rule matching
SLP-DEV1 Aug 26, 2026
1ebd194
ci: validate PR 10212 final review fixes
SLP-DEV1 Aug 26, 2026
6024f40
ci: speed up PR 10212 validation
SLP-DEV1 Aug 26, 2026
e95d8e7
fix(permissions): make env-prefix policy explicit
SLP-DEV1 Aug 26, 2026
cfcc658
Merge branch 'main' into fix/10197-env-prefix-bash-rules
SLP-DEV1 Aug 27, 2026
88b4a08
ci: validate PR 10212 review fixes
SLP-DEV1 Aug 27, 2026
00027a8
ci: run PR 10212 review validation
SLP-DEV1 Aug 27, 2026
2e91d65
ci: finalize PR 10212 review fixes
SLP-DEV1 Aug 27, 2026
9f54f6d
ci: repair PR 10212 finalizer
SLP-DEV1 Aug 27, 2026
57825c3
ci: repair PR 10212 patch generator
SLP-DEV1 Aug 27, 2026
f6367e7
ci: preserve env assignments across AST siblings
SLP-DEV1 Aug 27, 2026
d2e0073
ci: make env-prefix root patch whitespace tolerant
SLP-DEV1 Aug 27, 2026
d6807ac
Merge branch 'main' into fix/10197-env-prefix-bash-rules
SLP-DEV1 Aug 28, 2026
9c63e13
chore: remove PR-local finalize workflow
SLP-DEV1 Aug 29, 2026
99d464c
chore: remove embedded PR patch workflow
SLP-DEV1 Aug 29, 2026
a37589e
chore: remove PR-local round-3 workflow
SLP-DEV1 Aug 29, 2026
95b8245
fix(permissions): classify env-prefixed dangerous Bash rules
SLP-DEV1 Aug 29, 2026
7c0e52e
chore: stage PR 10212 review fix script
SLP-DEV1 Aug 29, 2026
0d6cb83
chore: run PR 10212 review fix validation
SLP-DEV1 Aug 29, 2026
34729bb
chore: apply PR fix before install build
SLP-DEV1 Aug 29, 2026
5fe7fa7
fix(core): address env-prefix permission review findings
SLP-DEV1 Aug 29, 2026
c8ec515
chore: update PR branch from upstream main
SLP-DEV1 Aug 29, 2026
e940ce1
Merge remote-tracking branch 'upstream/main' into fix/10197-env-prefi…
SLP-DEV1 Aug 29, 2026
1386698
chore: remove PR branch updater
SLP-DEV1 Aug 29, 2026
b7ec82b
Merge branch 'main' into fix/10197-env-prefix-bash-rules
SLP-DEV1 Aug 29, 2026
277c292
chore: apply reviewed PR 10212 R3 fixes
SLP-DEV1 Aug 30, 2026
6380b5e
chore: retry reviewed PR 10212 R3 fixes
SLP-DEV1 Aug 30, 2026
780d1df
chore: apply PR 10212 fixes with stable anchors
SLP-DEV1 Aug 30, 2026
4050d1c
chore: scope PR 10212 patch to target functions
SLP-DEV1 Aug 30, 2026
6dcd93e
chore: finish PR 10212 review fixes
SLP-DEV1 Aug 31, 2026
fa6b601
fix(core): close env-prefix permission review gaps
SLP-DEV1 Aug 31, 2026
463bcd0
Merge branch 'main' into fix/10197-env-prefix-bash-rules
SLP-DEV1 Aug 31, 2026
74bf30a
chore: apply PR 10212 round-4 fixes
SLP-DEV1 Aug 31, 2026
ffba09a
fix(core): harden env-prefix wildcard permission matching
github-actions[bot] Aug 31, 2026
4270b15
chore: remove temporary PR 10212 fixer
SLP-DEV1 Aug 31, 2026
04cf13e
Merge branch 'main' into fix/10197-env-prefix-bash-rules
SLP-DEV1 Aug 31, 2026
521db60
chore: apply PR 10212 matcher hardening
SLP-DEV1 Aug 31, 2026
e4c49dc
chore: remove temporary PR 10212 fixer
SLP-DEV1 Aug 31, 2026
48b9e14
fix(core): harden env-prefix Bash matcher
SLP-DEV1 Aug 31, 2026
72c0925
test(core): pin env-prefix matcher regressions
SLP-DEV1 Aug 31, 2026
da1a305
fix(core): keep restrictive Bash rules fail-closed
SLP-DEV1 Aug 31, 2026
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
5 changes: 4 additions & 1 deletion packages/core/src/permissions/dangerousRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

import { ToolNames } from '../tools/tool-names.js';
import { stripLeadingVariableAssignments } from './rule-parser.js';
import type { PermissionRule } from './types.js';

/**
Expand Down Expand Up @@ -171,7 +172,9 @@ export function isDangerousBashRule(rule: PermissionRule): boolean {

if (!rule.specifier || rule.specifier === '*') return true;

const content = rule.specifier.trim().toLowerCase();
const content = stripLeadingVariableAssignments(rule.specifier)
.trim()
.toLowerCase();
if (content === '' || content === '*') return true;

// Treat whitespace as the first-token delimiter; matcher-colon form is
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/permissions/permission-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,13 +341,13 @@ describe('matchesCommandPattern', () => {
expect(matchesCommandPattern('npm run *', 'npm run build')).toBe(true);
});

it('matches commands with leading env var assignments', async () => {
it('does not let env assignments inherit a glob Bash rule', async () => {
expect(
matchesCommandPattern(
'python3 *',
'PYTHONPATH=/tmp/lib python3 -c "print(1)"',
),
).toBe(true);
).toBe(false);
});

it('matches commands containing embedded newlines (dotAll)', async () => {
Expand Down
129 changes: 80 additions & 49 deletions packages/core/src/permissions/permission-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
matchesRule,
resolveToolName,
splitCompoundCommand,
stripLeadingVariableAssignments,
SHELL_TOOL_NAMES,
toolMatchesRuleToolName,
} from './rule-parser.js';
Expand Down Expand Up @@ -68,6 +69,78 @@ const DECISION_PRIORITY: Readonly<Record<PermissionDecision, number>> = {
allow: 0,
};

/**
* Restrictive shell rules retain the legacy no-env identity as an additional
* match shape. Both the command and an env-prefixed rule specifier are stripped
* together so tightening allow-rule identity can never make deny/ask fail open.
* Assignment-only restrictive rules deliberately have no stripped fallback:
* stripping them to an empty specifier would broaden them to the whole tool.
*/
function matchesRestrictiveRule(
rule: PermissionRule,
ctx: PermissionCheckContext,
pathCtx: PathMatchContext | undefined,
): boolean {
const {
toolName,
toolAliases,
command,
filePath,
domain,
specifier,
toolParams,
} = ctx;

const match = (
candidateRule: PermissionRule,
candidateCommand: string | undefined,
): boolean =>
matchesRule(
candidateRule,
toolName,
candidateCommand,
filePath,
domain,
pathCtx,
specifier,
toolParams,
toolAliases,
'canonical',
);

if (match(rule, command)) {
return true;
}

if (
command === undefined ||
!SHELL_TOOL_NAMES.has(resolveToolName(toolName))
) {
return false;
}

const strippedCommand = stripLeadingVariableAssignments(command);
if (!strippedCommand || strippedCommand === command) {
return false;
}

let fallbackRule = rule;
if (
rule.specifier !== undefined &&
(rule.specifierKind === 'command' || SHELL_TOOL_NAMES.has(rule.toolName))
) {
const strippedSpecifier = stripLeadingVariableAssignments(rule.specifier);
if (!strippedSpecifier) {
return false;
}
if (strippedSpecifier !== rule.specifier) {
fallbackRule = { ...rule, specifier: strippedSpecifier };
}
}

return match(fallbackRule, strippedCommand);
}

/**
* Minimal interface for the parts of Config used by PermissionManager.
* Keeps the dependency explicit and avoids a circular import on the
Expand Down Expand Up @@ -415,14 +488,14 @@ export class PermissionManager {
...this.sessionRules.deny,
...this.persistentRules.deny,
]) {
if (matchesRule(rule, ...matchArgs, 'canonical')) return 'deny';
if (matchesRestrictiveRule(rule, ctx, pathCtx)) return 'deny';
}
// Priority 2: ask rules
for (const rule of [
...this.sessionRules.ask,
...this.persistentRules.ask,
]) {
if (matchesRule(rule, ...matchArgs, 'canonical')) return 'ask';
if (matchesRestrictiveRule(rule, ctx, pathCtx)) return 'ask';
}
// Priority 3: allow rules
for (const rule of [
Expand Down Expand Up @@ -894,16 +967,7 @@ export class PermissionManager {
*/
findMatchingDenyRule(ctx: PermissionCheckContext): string | undefined {
ctx = this.normalizePermissionContext(ctx);
const {
toolName,
toolAliases,
command,
cwd,
filePath,
domain,
specifier,
toolParams,
} = ctx;
const { cwd } = ctx;

const pathCtx: PathMatchContext | undefined =
this.config.getProjectRoot && this.config.getCwd
Expand All @@ -913,22 +977,11 @@ export class PermissionManager {
}
: undefined;

const matchArgs = [
toolName,
command,
filePath,
domain,
pathCtx,
specifier,
toolParams,
toolAliases,
] as const;

for (const rule of [
...this.sessionRules.deny,
...this.persistentRules.deny,
]) {
if (matchesRule(rule, ...matchArgs, 'canonical')) {
if (matchesRestrictiveRule(rule, ctx, pathCtx)) {
return rule.raw;
}
}
Expand Down Expand Up @@ -1081,7 +1134,7 @@ export class PermissionManager {

return (
restrictiveRules.some((rule) =>
matchesRule(rule, ...matchArgs, 'canonical'),
matchesRestrictiveRule(rule, ctx, pathCtx),
) || allowRules.some((rule) => matchesRule(rule, ...matchArgs))
);
}
Expand All @@ -1097,16 +1150,7 @@ export class PermissionManager {
*/
hasMatchingAskRule(ctx: PermissionCheckContext): boolean {
ctx = this.normalizePermissionContext(ctx);
const {
toolName,
toolAliases,
command,
cwd,
filePath,
domain,
specifier,
toolParams,
} = ctx;
const { toolName, command, cwd } = ctx;

const pathCtx: PathMatchContext | undefined =
this.config.getProjectRoot && this.config.getCwd
Expand Down Expand Up @@ -1166,20 +1210,7 @@ export class PermissionManager {
}
}

const matchArgs = [
toolName,
command,
filePath,
domain,
pathCtx,
specifier,
toolParams,
toolAliases,
] as const;

return askRules.some((rule) =>
matchesRule(rule, ...matchArgs, 'canonical'),
);
return askRules.some((rule) => matchesRestrictiveRule(rule, ctx, pathCtx));
}

private hasAskRuleForTool(toolName: string): boolean {
Expand Down
Loading
Loading