axconfig 3.5.2 → 3.6.0

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.
@@ -45,11 +45,11 @@ function parseClaudeRule(rule) {
45
45
  if (parenIndex > 0 && rule.endsWith(")")) {
46
46
  const toolName = rule.slice(0, parenIndex);
47
47
  const pattern = rule.slice(parenIndex + 1, -1);
48
- // Bash pattern: "Bash(git:*)" → pattern is "git:*", we want "git"
48
+ // Bash pattern: "Bash(git:*)" → pattern is "git:*", we want "git*"
49
+ // Append * to indicate prefix matching (required by axconfig's canonical format)
49
50
  if (toolName === "Bash") {
50
- const bashPattern = pattern.endsWith(":*")
51
- ? pattern.slice(0, -2)
52
- : pattern;
51
+ const prefix = pattern.endsWith(":*") ? pattern.slice(0, -2) : pattern;
52
+ const bashPattern = prefix === "" ? "*" : `${prefix}*`;
53
53
  return { type: "bash", pattern: bashPattern };
54
54
  }
55
55
  // Path pattern: "Read(src/**)" → tool is "read", pattern is "src/**"
@@ -40,8 +40,15 @@ function translateRule(rule) {
40
40
  return TOOL_MAP[rule.name];
41
41
  }
42
42
  case "bash": {
43
- // Claude Code uses "Bash(pattern:*)" for command patterns
44
- return `Bash(${rule.pattern}:*)`;
43
+ // User provides pattern with explicit trailing *
44
+ // Strip it to get the prefix for Claude format
45
+ const prefix = rule.pattern.slice(0, -1);
46
+ // bash:* (all commands) → Bash (without parentheses)
47
+ // bash:git* → Bash(git:*)
48
+ if (prefix === "" || prefix.trim() === "") {
49
+ return "Bash";
50
+ }
51
+ return `Bash(${prefix}:*)`;
45
52
  }
46
53
  case "path": {
47
54
  // Claude Code uses "Tool(path/**)" for path patterns
@@ -90,12 +90,14 @@ function readPermissions(configDirectory) {
90
90
  // Read is always allowed in Codex sandbox
91
91
  allowRules.push({ type: "tool", name: "read" });
92
92
  // Parse bash patterns from rules files
93
+ // Append * to indicate prefix matching (required by axconfig's canonical format)
93
94
  for (const rule of rules) {
95
+ const pattern = rule.pattern === "" ? "*" : `${rule.pattern}*`;
94
96
  if (rule.decision === "allow") {
95
- allowRules.push({ type: "bash", pattern: rule.pattern });
97
+ allowRules.push({ type: "bash", pattern });
96
98
  }
97
99
  else {
98
- denyRules.push({ type: "bash", pattern: rule.pattern });
100
+ denyRules.push({ type: "bash", pattern });
99
101
  }
100
102
  }
101
103
  const permConfig = {
@@ -146,13 +146,17 @@ function build(config, output) {
146
146
  // Generate execpolicy rules
147
147
  const rules = ["# Generated by axconfig"];
148
148
  if (permissions) {
149
- // Collect bash patterns
149
+ // Collect bash patterns, stripping trailing * since Codex does prefix matching natively
150
+ // Filter out empty patterns (from bash:*) - Codex doesn't support empty prefix
151
+ // bash:* means "all commands" which is the default when no rules exist
150
152
  const allowBash = permissions.allow
151
153
  .filter((r) => r.type === "bash")
152
- .map((r) => r.pattern);
154
+ .map((r) => r.pattern.slice(0, -1).trim())
155
+ .filter((p) => p !== "");
153
156
  const denyBash = permissions.deny
154
157
  .filter((r) => r.type === "bash")
155
- .map((r) => r.pattern);
158
+ .map((r) => r.pattern.slice(0, -1).trim())
159
+ .filter((p) => p !== "");
156
160
  // Generate allow rules
157
161
  for (const pattern of allowBash) {
158
162
  rules.push(generatePrefixRule(pattern, "allow"));
@@ -73,12 +73,14 @@ function readPermissions(configDirectory) {
73
73
  ? rule.toolName
74
74
  : [rule.toolName];
75
75
  // Check for bash command patterns
76
+ // Append * to indicate prefix matching (required by axconfig's canonical format)
76
77
  if (rule.commandPrefix) {
77
78
  const prefixes = Array.isArray(rule.commandPrefix)
78
79
  ? rule.commandPrefix
79
80
  : [rule.commandPrefix];
80
81
  for (const prefix of prefixes) {
81
- targetList.push({ type: "bash", pattern: prefix });
82
+ const pattern = prefix === "" ? "*" : `${prefix}*`;
83
+ targetList.push({ type: "bash", pattern });
82
84
  }
83
85
  continue;
84
86
  }
@@ -112,23 +112,38 @@ function build(config, output) {
112
112
  const denyTools = permissions.deny
113
113
  .filter((r) => r.type === "tool")
114
114
  .map((r) => TOOL_MAP[r.name]);
115
- // Collect bash patterns
116
- const allowBash = permissions.allow
117
- .filter((r) => r.type === "bash")
118
- .map((r) => r.pattern);
119
- const denyBash = permissions.deny
120
- .filter((r) => r.type === "bash")
121
- .map((r) => r.pattern);
115
+ // Collect bash patterns, stripping trailing * since Gemini does prefix matching natively
116
+ // bash:* (empty prefix after strip) → allow run_shell_command tool entirely
117
+ // bash:git* commandPrefix = "git"
118
+ const allowBashPatterns = permissions.allow.filter((r) => r.type === "bash");
119
+ const denyBashPatterns = permissions.deny.filter((r) => r.type === "bash");
120
+ // Separate bash:* (all commands) from specific patterns
121
+ const allowAllBash = allowBashPatterns.some((r) => r.pattern.slice(0, -1).trim() === "");
122
+ const denyAllBash = denyBashPatterns.some((r) => r.pattern.slice(0, -1).trim() === "");
123
+ const allowBash = allowBashPatterns
124
+ .map((r) => r.pattern.slice(0, -1).trim())
125
+ .filter((p) => p !== "");
126
+ const denyBash = denyBashPatterns
127
+ .map((r) => r.pattern.slice(0, -1).trim())
128
+ .filter((p) => p !== "");
122
129
  // Generate allow rules (high priority)
123
- if (allowTools.length > 0) {
124
- rules.push(generateToolRule(allowTools, "allow", 999));
130
+ // Include run_shell_command if bash:* is in allow list
131
+ const effectiveAllowTools = allowAllBash
132
+ ? [...allowTools, TOOL_MAP.bash]
133
+ : allowTools;
134
+ if (effectiveAllowTools.length > 0) {
135
+ rules.push(generateToolRule(effectiveAllowTools, "allow", 999));
125
136
  }
126
137
  if (allowBash.length > 0) {
127
138
  rules.push(generateBashRule(allowBash, "allow", 998));
128
139
  }
129
140
  // Generate deny rules (medium priority)
130
- if (denyTools.length > 0) {
131
- rules.push(generateToolRule(denyTools, "deny", 500));
141
+ // Include run_shell_command if bash:* is in deny list
142
+ const effectiveDenyTools = denyAllBash
143
+ ? [...denyTools, TOOL_MAP.bash]
144
+ : denyTools;
145
+ if (effectiveDenyTools.length > 0) {
146
+ rules.push(generateToolRule(effectiveDenyTools, "deny", 500));
132
147
  }
133
148
  if (denyBash.length > 0) {
134
149
  rules.push(generateBashRule(denyBash, "deny", 499));
@@ -76,29 +76,6 @@ export function collectBashPatterns(rules) {
76
76
  .filter((r) => r.type === "bash")
77
77
  .map((r) => r.pattern);
78
78
  }
79
- /**
80
- * Normalize a bash pattern for OpenCode by adding a trailing wildcard if needed.
81
- *
82
- * OpenCode uses an "arity" system to extract command prefixes for permission
83
- * checking. For example, `gh` has arity 3, so `gh api repos/foo/bar` extracts
84
- * the prefix `gh api repos/foo/bar` (first 3 tokens). A pattern `gh api` won't
85
- * match because it's missing the third token.
86
- *
87
- * By appending `*` to patterns that don't already have wildcards, we ensure
88
- * patterns like `gh api` become `gh api*` which matches `gh api repos/...`.
89
- *
90
- * @example
91
- * normalizeBashPattern("gh api") // "gh api*"
92
- * normalizeBashPattern("git *") // "git *" (already has wildcard)
93
- * normalizeBashPattern("cat") // "cat*"
94
- */
95
- function normalizeBashPattern(pattern) {
96
- // Don't add another * if pattern already ends with one
97
- if (pattern.endsWith("*")) {
98
- return pattern;
99
- }
100
- return `${pattern}*`;
101
- }
102
79
  /**
103
80
  * Build bash permission config from patterns and tool permissions.
104
81
  *
@@ -124,10 +101,10 @@ export function buildBashPermission(allowPatterns, denyPatterns, bashAllowed, ba
124
101
  "*": bashAllowed ? "allow" : "deny",
125
102
  };
126
103
  for (const pattern of allowPatterns) {
127
- bashConfig[normalizeBashPattern(pattern)] = "allow";
104
+ bashConfig[pattern] = "allow";
128
105
  }
129
106
  for (const pattern of denyPatterns) {
130
- bashConfig[normalizeBashPattern(pattern)] = "deny";
107
+ bashConfig[pattern] = "deny";
131
108
  }
132
109
  return bashConfig;
133
110
  }
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Parses --allow and --deny CLI arguments into PermissionConfig.
5
5
  *
6
+ * Bash patterns require an explicit trailing wildcard (*) to indicate
7
+ * prefix matching. Patterns without wildcards will throw an error.
8
+ *
6
9
  * @example
7
10
  * parsePermissions(
8
11
  * ["read,glob,bash:git *"],
9
- * ["bash:rm *"]
12
+ * ["bash:rm*"]
10
13
  * )
11
14
  * // Returns:
12
15
  * // {
@@ -16,7 +19,7 @@
16
19
  * // { type: "bash", pattern: "git *" }
17
20
  * // ],
18
21
  * // deny: [
19
- * // { type: "bash", pattern: "rm *" }
22
+ * // { type: "bash", pattern: "rm*" }
20
23
  * // ]
21
24
  * // }
22
25
  */
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Parses --allow and --deny CLI arguments into PermissionConfig.
5
5
  *
6
+ * Bash patterns require an explicit trailing wildcard (*) to indicate
7
+ * prefix matching. Patterns without wildcards will throw an error.
8
+ *
6
9
  * @example
7
10
  * parsePermissions(
8
11
  * ["read,glob,bash:git *"],
9
- * ["bash:rm *"]
12
+ * ["bash:rm*"]
10
13
  * )
11
14
  * // Returns:
12
15
  * // {
@@ -16,7 +19,7 @@
16
19
  * // { type: "bash", pattern: "git *" }
17
20
  * // ],
18
21
  * // deny: [
19
- * // { type: "bash", pattern: "rm *" }
22
+ * // { type: "bash", pattern: "rm*" }
20
23
  * // ]
21
24
  * // }
22
25
  */
@@ -51,6 +54,11 @@ function parseRule(rule) {
51
54
  if (pattern === "") {
52
55
  throw new Error('Invalid bash pattern: "bash:" requires a command pattern');
53
56
  }
57
+ if (!pattern.endsWith("*")) {
58
+ throw new Error(`Bash pattern "${pattern}" requires a trailing wildcard.\n` +
59
+ `Use "bash:${pattern}*" to match commands starting with "${pattern}".\n` +
60
+ `All bash patterns are prefix matches.`);
61
+ }
54
62
  return { type: "bash", pattern };
55
63
  }
56
64
  // Check for path restriction: "read:src/**"
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axconfig",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "3.5.2",
5
+ "version": "3.6.0",
6
6
  "description": "Unified configuration management for AI coding agents - common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, and OpenCode",
7
7
  "repository": {
8
8
  "type": "git",