axconfig 3.5.2 → 3.6.1

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
@@ -1,6 +1,6 @@
1
1
  # axconfig
2
2
 
3
- Unified configuration management for AI coding agents — common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, and OpenCode.
3
+ Unified configuration management for AI coding agents — common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, GitHub Copilot CLI, and OpenCode.
4
4
 
5
5
  ## Overview
6
6
 
@@ -26,11 +26,11 @@ Unified syntax for all agents:
26
26
 
27
27
  ```bash
28
28
  # Tool permissions
29
- read, write, edit, bash, glob, grep, web
29
+ read, write, bash, glob, grep, webfetch
30
30
 
31
31
  # Bash command patterns
32
32
  bash:git *
33
- bash:npm run build
33
+ bash:npm run build*
34
34
 
35
35
  # Path restrictions (agent-dependent)
36
36
  read:src/**
@@ -45,19 +45,27 @@ Translates unified permissions to agent-specific formats:
45
45
  | -------- | --------------------------------------------------------- |
46
46
  | claude | JSON `settings.json` with `permissions.allow/deny` arrays |
47
47
  | codex | TOML `config.toml` + Starlark `.rules` files |
48
- | gemini | TOML policy files with `[[rule]]` entries |
48
+ | copilot | JSON `config.json` (no pre-configured permissions) |
49
+ | gemini | JSON `settings.json` + TOML policy files in `policies/` |
49
50
  | opencode | JSON with `permission.{edit,bash,webfetch}` |
50
51
 
51
52
  ### Capability Validation
52
53
 
53
54
  Each agent has different capabilities:
54
55
 
55
- | Agent | Tool Perms | Bash Patterns | Path Restrictions | Can Deny Read |
56
- | -------- | ----------- | ------------- | ----------------- | ------------- |
57
- | claude | ✓ | ✓ | ✓ | ✓ |
58
- | codex | ✗ (sandbox) | ✓ | ✗ | ✗ |
59
- | gemini | | | ✗ | |
60
- | opencode | ✓ | ✓ | ✗ | ✓ |
56
+ | Agent | Tool Perms | Bash Patterns | Path Restrictions | Can Deny Read | Notes |
57
+ | -------- | ----------- | ------------- | ----------------- | ------------- | -------------------------------------------- |
58
+ | claude | ✓ | ✓ | ✓ | ✓ | Permissions + OS-level sandbox |
59
+ | codex | ✗ (sandbox) | ✓ | ✗ | ✗ | OS-level sandbox (Landlock+seccomp/Seatbelt) |
60
+ | copilot | | | ✗ | | Runtime prompts only (no pre-config) |
61
+ | gemini | ✓ | ✓ | ✗ | ✓ | Permissions only (no sandbox) |
62
+ | opencode | ✓ | ✓ | ✓ | ✓ | UX-only permissions (no sandbox) |
63
+
64
+ **Security notes:**
65
+
66
+ - **Claude Code** manages application-level permissions; OS-level sandbox settings (network filtering, filesystem isolation) are configured separately
67
+ - **Copilot CLI** does not support pre-configured permissions; all actions require runtime approval prompts
68
+ - **OpenCode** permissions are UX-only to keep users informed; they do not provide security isolation
61
69
 
62
70
  axconfig validates permissions against agent capabilities:
63
71
 
@@ -217,6 +225,7 @@ src/
217
225
  └── agents/
218
226
  ├── claude.ts # Claude Code config builder + reader
219
227
  ├── codex.ts # Codex config builder + reader
228
+ ├── copilot.ts # GitHub Copilot CLI config builder + reader
220
229
  ├── gemini.ts # Gemini CLI config builder + reader
221
230
  └── opencode.ts # OpenCode config builder + reader
222
231
  ```
@@ -232,7 +241,7 @@ Run `npx -y axconfig --help` to learn available options.
232
241
 
233
242
  Use `axconfig` to manage AI agent configurations with unified permission syntax.
234
243
  It translates `--allow` and `--deny` rules to agent-specific formats (Claude Code,
235
- Codex, Gemini CLI, OpenCode).
244
+ Codex, Gemini CLI, GitHub Copilot CLI, OpenCode).
236
245
  ```
237
246
 
238
247
  ## License
@@ -13,15 +13,15 @@ import { createJsonConfigOperations, readJsonConfig, } from "../read-write-json-
13
13
  const REVERSE_TOOL_MAP = {
14
14
  Read: "read",
15
15
  Write: "write",
16
- Edit: "edit",
16
+ Edit: "write", // Claude's Edit tool maps to canonical "write"
17
17
  Bash: "bash",
18
18
  Glob: "glob",
19
19
  Grep: "grep",
20
- WebFetch: "web",
21
- WebSearch: "web",
20
+ WebFetch: "webfetch",
21
+ WebSearch: "webfetch",
22
22
  };
23
23
  /** Path-restricted tools (lowercase) */
24
- const PATH_TOOLS = new Set(["read", "write", "edit"]);
24
+ const PATH_TOOLS = new Set(["read", "write"]);
25
25
  /**
26
26
  * Get the settings.json path for a config directory.
27
27
  */
@@ -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/**"
@@ -17,12 +17,11 @@ export { claudeCodeConfigReader } from "./claude-reader.js";
17
17
  /** Claude Code tool name mapping */
18
18
  const TOOL_MAP = {
19
19
  read: "Read",
20
- write: "Write",
21
- edit: "Edit",
20
+ write: ["Write", "Edit"], // Canonical "write" maps to both Claude's Write and Edit tools
22
21
  bash: "Bash",
23
22
  glob: "Glob",
24
23
  grep: "Grep",
25
- web: "WebFetch",
24
+ webfetch: "WebFetch",
26
25
  };
27
26
  /** Claude Code capabilities */
28
27
  const CAPABILITIES = {
@@ -33,20 +32,33 @@ const CAPABILITIES = {
33
32
  };
34
33
  /**
35
34
  * Translate a permission rule to Claude Code format.
35
+ * Returns an array since some canonical tools map to multiple Claude tools.
36
36
  */
37
37
  function translateRule(rule) {
38
38
  switch (rule.type) {
39
39
  case "tool": {
40
- return TOOL_MAP[rule.name];
40
+ const mapped = TOOL_MAP[rule.name];
41
+ return Array.isArray(mapped) ? mapped : [mapped];
41
42
  }
42
43
  case "bash": {
43
- // Claude Code uses "Bash(pattern:*)" for command patterns
44
- return `Bash(${rule.pattern}:*)`;
44
+ // User provides pattern with explicit trailing *
45
+ // Strip it to get the prefix for Claude format
46
+ const prefix = rule.pattern.slice(0, -1);
47
+ // bash:* (all commands) → Bash (without parentheses)
48
+ // bash:git* → Bash(git:*)
49
+ if (prefix === "" || prefix.trim() === "") {
50
+ return ["Bash"];
51
+ }
52
+ return [`Bash(${prefix}:*)`];
45
53
  }
46
54
  case "path": {
47
55
  // Claude Code uses "Tool(path/**)" for path patterns
56
+ // write:pattern maps to both Write(pattern) and Edit(pattern)
57
+ if (rule.tool === "write") {
58
+ return [`Write(${rule.pattern})`, `Edit(${rule.pattern})`];
59
+ }
48
60
  const toolName = rule.tool.charAt(0).toUpperCase() + rule.tool.slice(1);
49
- return `${toolName}(${rule.pattern})`;
61
+ return [`${toolName}(${rule.pattern})`];
50
62
  }
51
63
  }
52
64
  }
@@ -96,9 +108,9 @@ function build(config, output) {
96
108
  };
97
109
  }
98
110
  // Claude Code supports all permission types, so no warnings/errors needed
99
- // All rules can be translated directly
100
- const allowRules = permissions.allow.map((rule) => translateRule(rule));
101
- const denyRules = permissions.deny.map((rule) => translateRule(rule));
111
+ // All rules can be translated directly (flatMap since translateRule returns arrays)
112
+ const allowRules = permissions.allow.flatMap((rule) => translateRule(rule));
113
+ const denyRules = permissions.deny.flatMap((rule) => translateRule(rule));
102
114
  const settings = {
103
115
  ...existingSettings,
104
116
  permissions: {
@@ -85,17 +85,19 @@ function readPermissions(configDirectory) {
85
85
  const sandboxMode = config.sandbox_mode;
86
86
  if (sandboxMode === "workspace-write" ||
87
87
  sandboxMode === "danger-full-access") {
88
- allowRules.push({ type: "tool", name: "write" }, { type: "tool", name: "edit" });
88
+ allowRules.push({ type: "tool", name: "write" });
89
89
  }
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 = {
@@ -26,11 +26,11 @@ const CAPABILITIES = {
26
26
  /**
27
27
  * Infer sandbox mode from permissions.
28
28
  *
29
- * - If write or edit is allowed → workspace-write
29
+ * - If write is allowed → workspace-write
30
30
  * - Otherwise → read-only
31
31
  */
32
32
  function inferSandboxMode(permissions) {
33
- const allowsWrite = permissions.allow.some((r) => r.type === "tool" && (r.name === "write" || r.name === "edit"));
33
+ const allowsWrite = permissions.allow.some((r) => r.type === "tool" && r.name === "write");
34
34
  return allowsWrite ? "workspace-write" : "read-only";
35
35
  }
36
36
  /**
@@ -98,9 +98,8 @@ function build(config, output) {
98
98
  });
99
99
  }
100
100
  }
101
- // Tool permissions other than read/write/edit - warn as Codex uses sandbox
102
- const nonFileTools = permissions.allow.filter((r) => r.type === "tool" &&
103
- !["read", "write", "edit", "bash"].includes(r.name));
101
+ // Tool permissions other than read/write - warn as Codex uses sandbox
102
+ const nonFileTools = permissions.allow.filter((r) => r.type === "tool" && !["read", "write", "bash"].includes(r.name));
104
103
  for (const rule of nonFileTools) {
105
104
  if (rule.type === "tool") {
106
105
  warnings.push({
@@ -146,13 +145,17 @@ function build(config, output) {
146
145
  // Generate execpolicy rules
147
146
  const rules = ["# Generated by axconfig"];
148
147
  if (permissions) {
149
- // Collect bash patterns
148
+ // Collect bash patterns, stripping trailing * since Codex does prefix matching natively
149
+ // Filter out empty patterns (from bash:*) - Codex doesn't support empty prefix
150
+ // bash:* means "all commands" which is the default when no rules exist
150
151
  const allowBash = permissions.allow
151
152
  .filter((r) => r.type === "bash")
152
- .map((r) => r.pattern);
153
+ .map((r) => r.pattern.slice(0, -1).trim())
154
+ .filter((p) => p !== "");
153
155
  const denyBash = permissions.deny
154
156
  .filter((r) => r.type === "bash")
155
- .map((r) => r.pattern);
157
+ .map((r) => r.pattern.slice(0, -1).trim())
158
+ .filter((p) => p !== "");
156
159
  // Generate allow rules
157
160
  for (const pattern of allowBash) {
158
161
  rules.push(generatePrefixRule(pattern, "allow"));
@@ -14,11 +14,11 @@ import { createJsonConfigOperations, readJsonConfig, } from "../read-write-json-
14
14
  const REVERSE_TOOL_MAP = {
15
15
  read_file: "read",
16
16
  write_file: "write",
17
- edit_file: "edit",
17
+ edit_file: "write", // Gemini's edit_file maps to canonical "write"
18
18
  run_shell_command: "bash",
19
19
  glob: "glob",
20
20
  grep: "grep",
21
- web_fetch: "web",
21
+ web_fetch: "webfetch",
22
22
  };
23
23
  /**
24
24
  * Get the settings.json path for a config directory.
@@ -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
  }
@@ -109,7 +111,7 @@ function readModel(configDirectory) {
109
111
  try {
110
112
  const settings = readSettings(configDirectory);
111
113
  const modelObject = settings.model;
112
- if (!modelObject || modelObject.name === undefined) {
114
+ if (modelObject?.name === undefined) {
113
115
  return { ok: true, value: undefined };
114
116
  }
115
117
  if (typeof modelObject.name !== "string") {
@@ -18,12 +18,11 @@ import { readExistingSettings } from "./gemini-settings.js";
18
18
  /** Gemini CLI tool name mapping */
19
19
  const TOOL_MAP = {
20
20
  read: "read_file",
21
- write: "write_file",
22
- edit: "edit_file",
21
+ write: ["write_file", "edit_file"], // Canonical "write" maps to both Gemini's write_file and edit_file
23
22
  bash: "run_shell_command",
24
23
  glob: "glob",
25
24
  grep: "grep",
26
- web: "web_fetch",
25
+ webfetch: "web_fetch",
27
26
  };
28
27
  /** Gemini CLI capabilities */
29
28
  const CAPABILITIES = {
@@ -106,29 +105,51 @@ function build(config, output) {
106
105
  const rules = [];
107
106
  if (permissions) {
108
107
  // Collect tool permissions (excluding path rules which were warned about)
108
+ // Use flatMap since canonical "write" maps to both write_file and edit_file
109
109
  const allowTools = permissions.allow
110
110
  .filter((r) => r.type === "tool")
111
- .map((r) => TOOL_MAP[r.name]);
111
+ .flatMap((r) => {
112
+ const mapped = TOOL_MAP[r.name];
113
+ return Array.isArray(mapped) ? mapped : [mapped];
114
+ });
112
115
  const denyTools = permissions.deny
113
116
  .filter((r) => r.type === "tool")
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);
117
+ .flatMap((r) => {
118
+ const mapped = TOOL_MAP[r.name];
119
+ return Array.isArray(mapped) ? mapped : [mapped];
120
+ });
121
+ // Collect bash patterns, stripping trailing * since Gemini does prefix matching natively
122
+ // bash:* (empty prefix after strip) → allow run_shell_command tool entirely
123
+ // bash:git* commandPrefix = "git"
124
+ const allowBashPatterns = permissions.allow.filter((r) => r.type === "bash");
125
+ const denyBashPatterns = permissions.deny.filter((r) => r.type === "bash");
126
+ // Separate bash:* (all commands) from specific patterns
127
+ const allowAllBash = allowBashPatterns.some((r) => r.pattern.slice(0, -1).trim() === "");
128
+ const denyAllBash = denyBashPatterns.some((r) => r.pattern.slice(0, -1).trim() === "");
129
+ const allowBash = allowBashPatterns
130
+ .map((r) => r.pattern.slice(0, -1).trim())
131
+ .filter((p) => p !== "");
132
+ const denyBash = denyBashPatterns
133
+ .map((r) => r.pattern.slice(0, -1).trim())
134
+ .filter((p) => p !== "");
122
135
  // Generate allow rules (high priority)
123
- if (allowTools.length > 0) {
124
- rules.push(generateToolRule(allowTools, "allow", 999));
136
+ // Include run_shell_command if bash:* is in allow list
137
+ const effectiveAllowTools = allowAllBash
138
+ ? [...allowTools, "run_shell_command"]
139
+ : allowTools;
140
+ if (effectiveAllowTools.length > 0) {
141
+ rules.push(generateToolRule(effectiveAllowTools, "allow", 999));
125
142
  }
126
143
  if (allowBash.length > 0) {
127
144
  rules.push(generateBashRule(allowBash, "allow", 998));
128
145
  }
129
146
  // Generate deny rules (medium priority)
130
- if (denyTools.length > 0) {
131
- rules.push(generateToolRule(denyTools, "deny", 500));
147
+ // Include run_shell_command if bash:* is in deny list
148
+ const effectiveDenyTools = denyAllBash
149
+ ? [...denyTools, "run_shell_command"]
150
+ : denyTools;
151
+ if (effectiveDenyTools.length > 0) {
152
+ rules.push(generateToolRule(effectiveDenyTools, "deny", 500));
132
153
  }
133
154
  if (denyBash.length > 0) {
134
155
  rules.push(generateBashRule(denyBash, "deny", 499));
@@ -32,9 +32,9 @@ export declare const BASE_PERMISSION_OVERRIDES: Record<string, OpenCodePermissio
32
32
  /**
33
33
  * Collect path patterns from permission rules, grouped by OpenCode permission name.
34
34
  *
35
- * Note: Only read/write/edit rules can have path patterns per the PathPatternRule type.
35
+ * Note: Only read/write rules can have path patterns per the PathPatternRule type.
36
36
  * The TOOL_TO_OPENCODE mapping includes glob/grep for tool-level permissions, but these
37
- * will never appear here since PathRestrictedTool is limited to read/write/edit.
37
+ * will never appear here since PathRestrictedTool is limited to read/write.
38
38
  */
39
39
  export declare function collectPathPatterns(rules: PermissionRule[]): Map<string, string[]>;
40
40
  /**
@@ -8,11 +8,10 @@
8
8
  */
9
9
  const TOOL_TO_OPENCODE = {
10
10
  read: "read",
11
- edit: "edit",
12
- write: "edit", // OpenCode uses single "edit" for both
11
+ write: "edit", // OpenCode uses single "edit" for write operations
13
12
  glob: "glob",
14
13
  grep: "grep",
15
- web: "webfetch",
14
+ webfetch: "webfetch",
16
15
  bash: "bash",
17
16
  };
18
17
  /**
@@ -42,9 +41,9 @@ export const BASE_PERMISSION_OVERRIDES = {
42
41
  /**
43
42
  * Collect path patterns from permission rules, grouped by OpenCode permission name.
44
43
  *
45
- * Note: Only read/write/edit rules can have path patterns per the PathPatternRule type.
44
+ * Note: Only read/write rules can have path patterns per the PathPatternRule type.
46
45
  * The TOOL_TO_OPENCODE mapping includes glob/grep for tool-level permissions, but these
47
- * will never appear here since PathRestrictedTool is limited to read/write/edit.
46
+ * will never appear here since PathRestrictedTool is limited to read/write.
48
47
  */
49
48
  export function collectPathPatterns(rules) {
50
49
  const patterns = new Map();
@@ -76,29 +75,6 @@ export function collectBashPatterns(rules) {
76
75
  .filter((r) => r.type === "bash")
77
76
  .map((r) => r.pattern);
78
77
  }
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
78
  /**
103
79
  * Build bash permission config from patterns and tool permissions.
104
80
  *
@@ -124,10 +100,10 @@ export function buildBashPermission(allowPatterns, denyPatterns, bashAllowed, ba
124
100
  "*": bashAllowed ? "allow" : "deny",
125
101
  };
126
102
  for (const pattern of allowPatterns) {
127
- bashConfig[normalizeBashPattern(pattern)] = "allow";
103
+ bashConfig[pattern] = "allow";
128
104
  }
129
105
  for (const pattern of denyPatterns) {
130
- bashConfig[normalizeBashPattern(pattern)] = "deny";
106
+ bashConfig[pattern] = "deny";
131
107
  }
132
108
  return bashConfig;
133
109
  }
@@ -143,14 +119,8 @@ export function buildBashPermission(allowPatterns, denyPatterns, bashAllowed, ba
143
119
  export function buildToolPermission(canonicalName, opencodeName, allowedTools, deniedTools, allowPathPatterns, denyPathPatterns) {
144
120
  const hasAllowPatterns = allowPathPatterns.has(opencodeName);
145
121
  const hasDenyPatterns = denyPathPatterns.has(opencodeName);
146
- // Handle write -> edit mapping (both map to same OpenCode permission)
147
- const isEditOrWrite = canonicalName === "edit" || canonicalName === "write";
148
- const effectiveAllowed = isEditOrWrite
149
- ? allowedTools.has("edit") || allowedTools.has("write")
150
- : allowedTools.has(canonicalName);
151
- const effectiveDenied = isEditOrWrite
152
- ? deniedTools.has("edit") || deniedTools.has("write")
153
- : deniedTools.has(canonicalName);
122
+ const effectiveAllowed = allowedTools.has(canonicalName);
123
+ const effectiveDenied = deniedTools.has(canonicalName);
154
124
  if (effectiveDenied && !hasDenyPatterns && !hasAllowPatterns) {
155
125
  return "deny";
156
126
  }
@@ -23,10 +23,10 @@ import { createJsonConfigOperations, readJsonConfig, } from "../read-write-json-
23
23
  */
24
24
  const OPENCODE_TO_TOOL = {
25
25
  read: "read",
26
- edit: ["edit", "write"], // OpenCode's "edit" maps to both canonical tools
26
+ edit: "write", // OpenCode's "edit" maps to canonical "write"
27
27
  glob: "glob",
28
28
  grep: "grep",
29
- webfetch: "web",
29
+ webfetch: "webfetch",
30
30
  bash: "bash",
31
31
  };
32
32
  /**
@@ -55,18 +55,13 @@ function isAllowAction(action) {
55
55
  * Parse a permission value (simple or object-based) and add rules to the arrays.
56
56
  */
57
57
  function parsePermissionValue(opencodeName, value, allowRules, denyRules) {
58
- const canonicalTools = OPENCODE_TO_TOOL[opencodeName];
59
- if (!canonicalTools)
58
+ const canonicalTool = OPENCODE_TO_TOOL[opencodeName];
59
+ if (!canonicalTool)
60
60
  return; // Unknown permission, skip
61
- const tools = Array.isArray(canonicalTools)
62
- ? canonicalTools
63
- : [canonicalTools];
64
61
  if (typeof value === "string") {
65
62
  // Simple value: "allow", "deny", or "ask"
66
63
  const rules = isAllowAction(value) ? allowRules : denyRules;
67
- for (const tool of tools) {
68
- rules.push({ type: "tool", name: tool });
69
- }
64
+ rules.push({ type: "tool", name: canonicalTool });
70
65
  }
71
66
  else if (typeof value === "object") {
72
67
  // Object with patterns
@@ -74,9 +69,7 @@ function parsePermissionValue(opencodeName, value, allowRules, denyRules) {
74
69
  const rules = isAllowAction(action) ? allowRules : denyRules;
75
70
  if (pattern === "*") {
76
71
  // Catch-all represents tool-level permission
77
- for (const tool of tools) {
78
- rules.push({ type: "tool", name: tool });
79
- }
72
+ rules.push({ type: "tool", name: canonicalTool });
80
73
  continue;
81
74
  }
82
75
  if (opencodeName === "bash") {
@@ -84,13 +77,11 @@ function parsePermissionValue(opencodeName, value, allowRules, denyRules) {
84
77
  rules.push({ type: "bash", pattern });
85
78
  }
86
79
  else {
87
- // Only read/write/edit support path patterns in axconfig's type system.
80
+ // Only read/write support path patterns in axconfig's type system.
88
81
  // OpenCode's glob/grep patterns match against glob patterns or regexes,
89
82
  // not file paths, so they're intentionally excluded here.
90
- for (const tool of tools) {
91
- if (tool === "read" || tool === "write" || tool === "edit") {
92
- rules.push({ type: "path", tool, pattern });
93
- }
83
+ if (canonicalTool === "read" || canonicalTool === "write") {
84
+ rules.push({ type: "path", tool: canonicalTool, pattern });
94
85
  }
95
86
  }
96
87
  }
@@ -40,10 +40,10 @@ function buildPermissionConfig(permissions) {
40
40
  // Set permissions for standard tools
41
41
  const tools = [
42
42
  { canonical: "read", opencode: "read" },
43
- { canonical: "edit", opencode: "edit" },
43
+ { canonical: "write", opencode: "edit" }, // OpenCode uses "edit" for write operations
44
44
  { canonical: "glob", opencode: "glob" },
45
45
  { canonical: "grep", opencode: "grep" },
46
- { canonical: "web", opencode: "webfetch" },
46
+ { canonical: "webfetch", opencode: "webfetch" },
47
47
  ];
48
48
  for (const { canonical, opencode } of tools) {
49
49
  permissionConfig[opencode] = buildToolPermission(canonical, opencode, allowedTools, deniedTools, allowPathPatterns, denyPathPatterns);
@@ -85,7 +85,7 @@ function build(config, output) {
85
85
  };
86
86
  atomicWriteFileSync(configPath, JSON.stringify(openCodeConfig, undefined, 2));
87
87
  // Build environment variables:
88
- // - OPENCODE_CONFIG_DIR: axpoint convention for config directory
88
+ // - OPENCODE_CONFIG_DIR: axkit convention for config directory
89
89
  // - XDG_DATA_HOME/XDG_CONFIG_HOME: OpenCode uses these for auth and other XDG-based paths
90
90
  const runtimeEnvironment = buildAgentRuntimeEnvironment("opencode", output);
91
91
  return {
@@ -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
  */
@@ -24,14 +27,13 @@
24
27
  const CANONICAL_TOOLS = new Set([
25
28
  "read",
26
29
  "write",
27
- "edit",
28
30
  "bash",
29
31
  "glob",
30
32
  "grep",
31
- "web",
33
+ "webfetch",
32
34
  ]);
33
35
  /** Tools that support path restrictions */
34
- const PATH_TOOLS = new Set(["read", "write", "edit"]);
36
+ const PATH_TOOLS = new Set(["read", "write"]);
35
37
  /**
36
38
  * Parse a single permission rule string.
37
39
  *
@@ -51,6 +53,11 @@ function parseRule(rule) {
51
53
  if (pattern === "") {
52
54
  throw new Error('Invalid bash pattern: "bash:" requires a command pattern');
53
55
  }
56
+ if (!pattern.endsWith("*")) {
57
+ throw new Error(`Bash pattern "${pattern}" requires a trailing wildcard.\n` +
58
+ `Use "bash:${pattern}*" to match commands starting with "${pattern}".\n` +
59
+ `All bash patterns are prefix matches.`);
60
+ }
54
61
  return { type: "bash", pattern };
55
62
  }
56
63
  // Check for path restriction: "read:src/**"
package/dist/types.d.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  */
6
6
  import type { AgentCli } from "axshared";
7
7
  /** Canonical tool names used in axrun permissions */
8
- type CanonicalTool = "read" | "write" | "edit" | "bash" | "glob" | "grep" | "web";
8
+ type CanonicalTool = "read" | "write" | "bash" | "glob" | "grep" | "webfetch";
9
9
  /** Tools that support path restrictions */
10
- type PathRestrictedTool = "read" | "write" | "edit";
10
+ type PathRestrictedTool = "read" | "write";
11
11
  /** Permission rule for a tool (e.g., "read", "bash") */
12
12
  interface ToolPermissionRule {
13
13
  type: "tool";
package/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "name": "axconfig",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "3.5.2",
6
- "description": "Unified configuration management for AI coding agents - common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, and OpenCode",
5
+ "version": "3.6.1",
6
+ "description": "Unified configuration management for AI coding agents - common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, GitHub Copilot CLI, and OpenCode",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/Jercik/axconfig.git"
@@ -55,6 +55,7 @@
55
55
  "configuration",
56
56
  "claude-code",
57
57
  "codex",
58
+ "copilot-cli",
58
59
  "gemini",
59
60
  "opencode",
60
61
  "llm",
@@ -68,22 +69,22 @@
68
69
  "dependencies": {
69
70
  "@commander-js/extra-typings": "^14.0.0",
70
71
  "@iarna/toml": "^2.2.5",
71
- "axshared": "^1.7.1",
72
+ "axshared": "^1.9.0",
72
73
  "commander": "^14.0.2"
73
74
  },
74
75
  "devDependencies": {
75
76
  "@total-typescript/ts-reset": "^0.6.1",
76
77
  "@types/iarna__toml": "^2.0.5",
77
- "@types/node": "^25.0.3",
78
- "@vitest/coverage-v8": "^4.0.16",
78
+ "@types/node": "^25.0.6",
79
+ "@vitest/coverage-v8": "^4.0.17",
79
80
  "eslint": "^9.39.2",
80
- "eslint-config-axpoint": "^1.0.0",
81
+ "eslint-config-axkit": "^1.1.0",
81
82
  "fta-check": "^1.5.1",
82
83
  "fta-cli": "^3.0.0",
83
- "knip": "^5.79.0",
84
+ "knip": "^5.80.2",
84
85
  "prettier": "3.7.4",
85
86
  "semantic-release": "^25.0.2",
86
87
  "typescript": "^5.9.3",
87
- "vitest": "^4.0.16"
88
+ "vitest": "^4.0.17"
88
89
  }
89
90
  }