axconfig 1.1.0 → 3.0.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.
Files changed (88) hide show
  1. package/README.md +124 -141
  2. package/dist/agents/claude-reader.d.ts +9 -0
  3. package/dist/agents/claude-reader.js +164 -0
  4. package/dist/agents/{claude-code.d.ts → claude.d.ts} +1 -0
  5. package/dist/agents/{claude-code.js → claude.js} +30 -4
  6. package/dist/agents/codex-reader.d.ts +9 -0
  7. package/dist/agents/codex-reader.js +166 -0
  8. package/dist/agents/codex.d.ts +1 -0
  9. package/dist/agents/codex.js +28 -10
  10. package/dist/agents/copilot-reader.d.ts +13 -0
  11. package/dist/agents/copilot-reader.js +103 -0
  12. package/dist/agents/copilot.d.ts +16 -0
  13. package/dist/agents/copilot.js +85 -0
  14. package/dist/agents/gemini-reader.d.ts +9 -0
  15. package/dist/agents/gemini-reader.js +162 -0
  16. package/dist/agents/gemini.d.ts +1 -0
  17. package/dist/agents/gemini.js +29 -6
  18. package/dist/agents/opencode-reader.d.ts +9 -0
  19. package/dist/agents/opencode-reader.js +139 -0
  20. package/dist/agents/opencode.d.ts +1 -0
  21. package/dist/agents/opencode.js +30 -6
  22. package/dist/atomic-write.d.ts +19 -0
  23. package/dist/atomic-write.js +46 -0
  24. package/dist/build-agent-config.d.ts +3 -2
  25. package/dist/builder.d.ts +3 -2
  26. package/dist/cli.d.ts +2 -1
  27. package/dist/cli.js +58 -55
  28. package/dist/commands/create.d.ts +1 -2
  29. package/dist/commands/create.js +6 -60
  30. package/dist/commands/get-raw.d.ts +11 -0
  31. package/dist/commands/get-raw.js +66 -0
  32. package/dist/commands/get.d.ts +11 -0
  33. package/dist/commands/get.js +136 -0
  34. package/dist/commands/set-permissions.d.ts +18 -0
  35. package/dist/commands/set-permissions.js +99 -0
  36. package/dist/commands/set-raw.d.ts +10 -0
  37. package/dist/commands/set-raw.js +58 -0
  38. package/dist/commands/set.d.ts +11 -0
  39. package/dist/commands/set.js +72 -0
  40. package/dist/get-agent-runtime-environment.d.ts +37 -0
  41. package/dist/get-agent-runtime-environment.js +48 -0
  42. package/dist/index.d.ts +12 -28
  43. package/dist/index.js +14 -25
  44. package/dist/object-path.d.ts +38 -0
  45. package/dist/object-path.js +104 -0
  46. package/dist/read-write-json-config.d.ts +26 -0
  47. package/dist/read-write-json-config.js +99 -0
  48. package/dist/read-write-toml-config.d.ts +26 -0
  49. package/dist/read-write-toml-config.js +100 -0
  50. package/dist/reader.d.ts +20 -0
  51. package/dist/reader.js +27 -0
  52. package/dist/resolve-config-path.d.ts +21 -0
  53. package/dist/resolve-config-path.js +23 -0
  54. package/dist/types.d.ts +82 -6
  55. package/dist/types.js +1 -3
  56. package/package.json +5 -3
  57. package/dist/auth/agents/claude-code.d.ts +0 -15
  58. package/dist/auth/agents/claude-code.js +0 -160
  59. package/dist/auth/agents/codex-config.d.ts +0 -12
  60. package/dist/auth/agents/codex-config.js +0 -79
  61. package/dist/auth/agents/codex-storage.d.ts +0 -25
  62. package/dist/auth/agents/codex-storage.js +0 -43
  63. package/dist/auth/agents/codex.d.ts +0 -15
  64. package/dist/auth/agents/codex.js +0 -139
  65. package/dist/auth/agents/gemini.d.ts +0 -15
  66. package/dist/auth/agents/gemini.js +0 -149
  67. package/dist/auth/agents/opencode.d.ts +0 -15
  68. package/dist/auth/agents/opencode.js +0 -102
  69. package/dist/auth/check-auth.d.ts +0 -13
  70. package/dist/auth/check-auth.js +0 -24
  71. package/dist/auth/extract-credentials.d.ts +0 -11
  72. package/dist/auth/extract-credentials.js +0 -20
  73. package/dist/auth/file-storage.d.ts +0 -14
  74. package/dist/auth/file-storage.js +0 -63
  75. package/dist/auth/get-access-token.d.ts +0 -35
  76. package/dist/auth/get-access-token.js +0 -124
  77. package/dist/auth/install-credentials.d.ts +0 -21
  78. package/dist/auth/install-credentials.js +0 -21
  79. package/dist/auth/keychain.d.ts +0 -12
  80. package/dist/auth/keychain.js +0 -56
  81. package/dist/auth/remove-credentials.d.ts +0 -15
  82. package/dist/auth/remove-credentials.js +0 -20
  83. package/dist/auth/types.d.ts +0 -19
  84. package/dist/auth/types.js +0 -4
  85. package/dist/commands/auth.d.ts +0 -32
  86. package/dist/commands/auth.js +0 -159
  87. package/dist/crypto.d.ts +0 -39
  88. package/dist/crypto.js +0 -78
package/README.md CHANGED
@@ -1,38 +1,24 @@
1
1
  # axconfig
2
2
 
3
- Configuration management library and CLI for AI coding agents.
3
+ Unified configuration management for AI coding agents — common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, and OpenCode.
4
4
 
5
5
  ## Overview
6
6
 
7
- axconfig is the **single source of truth** for all AI coding agent configuration logic. It can be used:
7
+ Different AI coding agents each have their own config formats, permission syntaxes, and settings locations. If you want to control what an agent can do or configure its behavior, you'd need to learn each agent's specific configuration format and file locations.
8
8
 
9
- 1. **As a library** - imported by [axrun](https://github.com/anthropics/axrun) to build agent configs programmatically
10
- 2. **As a CLI** - standalone tool for managing, inspecting, and creating agent configs
9
+ axconfig solves this by providing:
11
10
 
12
- ## Architecture
11
+ 1. **Unified permission syntax** — One way to express permissions (`--allow read,bash:git *`) that works across all agents
12
+ 2. **Common config access** — Get and set configuration properties using a consistent API, regardless of agent
13
+ 3. **Translation to agent-specific formats** — Converts unified config into whatever format each agent expects
14
+ 4. **Capability validation** — Knows what each agent supports and warns/errors appropriately
13
15
 
14
- ```
15
- ┌─────────────────────────────────────────────────────────────┐
16
- │ axrun │
17
- │ (Agent execution: spawns agents, streams events) │
18
- │ │
19
- │ axrun -a claude-code --allow "read,bash:git *" "prompt" │
20
- │ │ │
21
- │ ▼ │
22
- │ ┌────────────────────────┐ │
23
- │ │ axconfig │ │
24
- │ │ (Config translation) │ │
25
- │ └────────────────────────┘ │
26
- │ │ │
27
- │ ▼ │
28
- │ { env, settings } │
29
- │ │ │
30
- │ ▼ │
31
- │ spawn(claude, { env }) │
32
- └─────────────────────────────────────────────────────────────┘
33
- ```
16
+ It can be used:
17
+
18
+ - **As a library** — integrate into your own tools and scripts
19
+ - **As a CLI** standalone tool for managing agent configs
34
20
 
35
- ## Core Responsibilities
21
+ ## Core Functionality
36
22
 
37
23
  ### Permission Parsing
38
24
 
@@ -55,23 +41,23 @@ write:.env
55
41
 
56
42
  Translates unified permissions to agent-specific formats:
57
43
 
58
- | Agent | Output Format |
59
- | ----------- | --------------------------------------------------------- |
60
- | claude-code | JSON `settings.json` with `permissions.allow/deny` arrays |
61
- | codex | TOML `config.toml` + Starlark `.rules` files |
62
- | gemini | TOML policy files with `[[rule]]` entries |
63
- | opencode | JSON with `permission.{edit,bash,webfetch}` |
44
+ | Agent | Output Format |
45
+ | -------- | --------------------------------------------------------- |
46
+ | claude | JSON `settings.json` with `permissions.allow/deny` arrays |
47
+ | codex | TOML `config.toml` + Starlark `.rules` files |
48
+ | gemini | TOML policy files with `[[rule]]` entries |
49
+ | opencode | JSON with `permission.{edit,bash,webfetch}` |
64
50
 
65
51
  ### Capability Validation
66
52
 
67
53
  Each agent has different capabilities:
68
54
 
69
- | Agent | Tool Perms | Bash Patterns | Path Restrictions | Can Deny Read |
70
- | ----------- | ----------- | ------------- | ----------------- | ------------- |
71
- | claude-code | ✓ | ✓ | ✓ | ✓ |
72
- | codex | ✗ (sandbox) | ✓ | ✗ | ✗ |
73
- | gemini | ✓ | ✓ | ✗ | ✓ |
74
- | opencode | ✓ | ✓ | ✗ | ✓ |
55
+ | Agent | Tool Perms | Bash Patterns | Path Restrictions | Can Deny Read |
56
+ | -------- | ----------- | ------------- | ----------------- | ------------- |
57
+ | claude | ✓ | ✓ | ✓ | ✓ |
58
+ | codex | ✗ (sandbox) | ✓ | ✗ | ✗ |
59
+ | gemini | ✓ | ✓ | ✗ | ✓ |
60
+ | opencode | ✓ | ✓ | ✗ | ✓ |
75
61
 
76
62
  axconfig validates permissions against agent capabilities:
77
63
 
@@ -81,7 +67,12 @@ axconfig validates permissions against agent capabilities:
81
67
  ## Library API
82
68
 
83
69
  ```typescript
84
- import { parsePermissions, getConfigBuilder, buildAgentConfig } from "axconfig";
70
+ import {
71
+ buildAgentConfig,
72
+ getConfigReader,
73
+ parsePermissions,
74
+ resolveConfigPath,
75
+ } from "axconfig";
85
76
 
86
77
  // Parse CLI-style permission strings
87
78
  const permissions = parsePermissions(
@@ -91,7 +82,7 @@ const permissions = parsePermissions(
91
82
 
92
83
  // Build agent-specific config
93
84
  const result = buildAgentConfig({
94
- agentId: "claude-code",
85
+ agentId: "claude",
95
86
  allow: "read,glob,bash:git *",
96
87
  deny: "bash:rm *",
97
88
  output: "/tmp/my-config", // directory for config files
@@ -101,136 +92,134 @@ if (result.ok) {
101
92
  // result.env = { CLAUDE_CONFIG_DIR: "/tmp/my-config" }
102
93
  // result.warnings = [...]
103
94
  }
104
- ```
105
95
 
106
- ## CLI Commands
96
+ // Read existing config
97
+ const reader = getConfigReader("claude");
98
+ const configDir = resolveConfigPath("claude"); // ~/.claude/
107
99
 
108
- ```bash
109
- # List agents and their auth status
110
- axconfig auth list
111
- axconfig auth list --json
100
+ const perms = reader.readPermissions(configDir);
101
+ if (perms.ok && perms.value) {
102
+ console.log(perms.value.allow); // PermissionRule[]
103
+ }
104
+
105
+ // Read/write raw config values
106
+ const raw = reader.readRaw(configDir, "permissions.allow");
107
+ reader.writeRaw(configDir, "customSetting", { foo: "bar" });
108
+ ```
112
109
 
113
- # Get access token for an agent (outputs raw token for piping)
114
- axconfig auth token --agent claude-code
110
+ ## CLI Commands
115
111
 
116
- # Export credentials to encrypted file
117
- axconfig auth export --agent claude-code --output creds.json
118
- axconfig auth export --agent claude-code --output creds.json --no-password
112
+ ### Create Config
119
113
 
114
+ ```bash
120
115
  # Create config with permissions (outputs env vars)
121
- axconfig create --agent claude-code --output /tmp/config \
116
+ axconfig create --agent claude --output /tmp/config \
122
117
  --allow "read,glob,bash:git *" \
123
118
  --deny "bash:rm *"
124
119
 
125
- # Create config with exported credentials
126
- axconfig create --agent claude-code --output /tmp/config \
127
- --allow read \
128
- --with-credentials creds.json
129
-
130
120
  # Export for shell usage
131
- eval $(axconfig create --agent claude-code --output /tmp/config --allow read)
132
- ```
121
+ eval $(axconfig create --agent claude --output /tmp/config --allow read)
133
122
 
134
- ## Pipeline Examples
123
+ # Export as JSON for parsing
124
+ axconfig create --agent claude --output /tmp/cfg --allow read --format json | jq '.env'
125
+ ```
135
126
 
136
- The CLI outputs TSV format for easy processing with standard Unix tools:
127
+ ### Get/Set Unified Settings
137
128
 
138
129
  ```bash
139
- # List all agents and their auth status
140
- axconfig auth list
141
- # AGENT STATUS METHOD
142
- # claude-code authenticated OAuth (max)
143
- # codex authenticated ChatGPT OAuth
144
- # ...
130
+ # Get current settings (uses agent's default config location)
131
+ axconfig get --agent claude allow
132
+ # Output: read,glob,bash:git *
145
133
 
146
- # Filter to show only authenticated agents
147
- axconfig auth list | tail -n +2 | awk -F'\t' '$2 == "authenticated"'
134
+ axconfig get --agent claude deny
135
+ # Output: bash:rm *
148
136
 
149
- # Count agents by status
150
- axconfig auth list | tail -n +2 | cut -f2 | sort | uniq -c
137
+ axconfig get --agent claude model
138
+ # Output: sonnet
151
139
 
152
- # Extract agent names as a list
153
- axconfig auth list | tail -n +2 | cut -f1
140
+ # Get settings as JSON
141
+ axconfig get --agent claude allow --format json
142
+ # Output: ["Read", "Glob", {"Bash": {"command": "git", "args": "*"}}]
154
143
 
155
- # Export config as JSON and extract specific fields with jq
156
- axconfig create --agent claude-code --output /tmp/cfg --allow read --format json | jq '.env'
144
+ # Set settings (merge with existing by default)
145
+ axconfig set --agent claude allow "read,glob"
146
+ axconfig set --agent claude deny "bash:rm *"
147
+ axconfig set --agent claude model "claude-sonnet-4-20250514"
157
148
 
158
- # Check if a specific agent is authenticated
159
- axconfig auth list --json | jq -e '.[] | select(.agentId == "claude-code") | .authenticated'
149
+ # Replace instead of merge (for allow/deny)
150
+ axconfig set --agent claude allow "read,glob" --replace
160
151
 
161
- # Check Claude Code usage via OAuth endpoint
162
- curl -s -H "Authorization: Bearer $(axconfig auth token --agent claude-code)" \
163
- -H "anthropic-beta: oauth-2025-04-20" \
164
- https://api.anthropic.com/api/oauth/usage | jq .
152
+ # Set with custom config path
153
+ axconfig set --agent claude --path /tmp/cfg allow "read,glob"
165
154
  ```
166
155
 
167
- ## Module Structure
156
+ ### Get/Set Raw Config Values
168
157
 
169
- ```
170
- src/
171
- ├── types.ts # PermissionRule, AxrunConfig, BuildResult, etc.
172
- ├── parse-permissions.ts # parseRule, parseRuleList, parsePermissions
173
- ├── builder.ts # ConfigBuilder registry
174
- ├── build-agent-config.ts # High-level buildAgentConfig function
175
- ├── check-auth.ts # Auth status detection for agents
176
- ├── extract-credentials.ts # Credential extraction for export
177
- ├── crypto.ts # AES-256-GCM encryption for credentials
178
- ├── cli.ts # CLI entry point
179
- └── agents/
180
- ├── claude-code.ts # Claude Code config builder
181
- ├── codex.ts # Codex config builder
182
- ├── gemini.ts # Gemini CLI config builder
183
- └── opencode.ts # OpenCode config builder
184
- ```
158
+ ```bash
159
+ # Get raw config value by dotted path
160
+ axconfig get-raw --agent claude permissions.allow
161
+ # Output: ["Read", "Glob"]
185
162
 
186
- ## Integration with axrun
163
+ # Get raw value as JSON
164
+ axconfig get-raw --agent claude permissions --format json
187
165
 
188
- axrun imports axconfig as a dependency:
166
+ # Set raw config value (JSON or string)
167
+ axconfig set-raw --agent claude permissions.allow '["Read", "Glob"]'
168
+ axconfig set-raw --agent opencode permission.edit allow
169
+ ```
189
170
 
190
- ```typescript
191
- // In axrun/src/cli.ts
192
- import { buildAgentConfig } from "axconfig";
193
-
194
- const configResult = buildAgentConfig({
195
- agentId: options.agent,
196
- allow: options.allow,
197
- deny: options.deny,
198
- output: "/tmp/axrun-config",
199
- });
171
+ ### Pipeline Examples
200
172
 
201
- if (!configResult.ok) {
202
- // Handle errors
203
- }
173
+ ```bash
174
+ # Capture settings in shell variables
175
+ ALLOW=$(axconfig get --agent claude allow)
176
+ MODEL=$(axconfig get --agent claude model)
204
177
 
205
- // Spawn agent with config
206
- spawn(agentBin, args, { env: { ...process.env, ...configResult.env } });
207
- ```
178
+ # Extract allow rules as JSON and filter with jq
179
+ axconfig get --agent claude allow --format json | jq '.'
208
180
 
209
- ## Auth Preservation
181
+ # List all bash permissions, one per line
182
+ axconfig get --agent claude allow | tr ',' '\n' | grep 'bash:'
210
183
 
211
- A key requirement is preserving agent authentication when injecting permissions.
184
+ # Count unique bash command prefixes
185
+ axconfig get --agent claude allow \
186
+ | tr ',' '\n' | grep 'bash:' \
187
+ | cut -d: -f2 | cut -d' ' -f1 | sort | uniq -c | sort -rn
212
188
 
213
- ### Problem
189
+ # Compare permissions between agents
190
+ diff <(axconfig get -a claude allow) <(axconfig get -a gemini allow)
214
191
 
215
- Agents store auth in their config directories. Naively overriding config paths breaks auth:
192
+ # Check if a specific permission exists
193
+ axconfig get --agent claude allow | grep -q 'bash:git' && echo "git allowed"
216
194
 
217
- ```bash
218
- # This breaks auth!
219
- CLAUDE_CONFIG_DIR=/tmp/custom claude "prompt"
195
+ # Add a new permission to existing allow list
196
+ CURRENT=$(axconfig get --agent claude allow)
197
+ axconfig set --agent claude allow "$CURRENT,write" --replace
220
198
  ```
221
199
 
222
- ### Solution
223
-
224
- Each agent has a preferred method for layering config:
225
-
226
- | Agent | Method |
227
- | ----------- | ---------------------------------------------------- |
228
- | claude-code | `--settings <file>` flag merges with existing config |
229
- | codex | `CODEX_HOME` with copied/symlinked auth files |
230
- | gemini | `GEMINI_CLI_SYSTEM_SETTINGS_PATH` for settings |
231
- | opencode | `OPENCODE_CONFIG` points to merged config |
200
+ ## Module Structure
232
201
 
233
- axconfig handles these differences internally.
202
+ ```
203
+ src/
204
+ ├── types.ts # PermissionRule, ConfigReader, BuildResult, etc.
205
+ ├── parse-permissions.ts # parseRule, parseRuleList, parsePermissions
206
+ ├── builder.ts # ConfigBuilder registry
207
+ ├── reader.ts # ConfigReader registry
208
+ ├── resolve-config-path.ts # Resolve agent config directory
209
+ ├── build-agent-config.ts # High-level buildAgentConfig function
210
+ ├── cli.ts # CLI entry point
211
+ ├── commands/
212
+ │ ├── create.ts # Create command handler
213
+ │ ├── get.ts # Get unified settings
214
+ │ ├── set.ts # Set unified settings
215
+ │ ├── get-raw.ts # Get raw config values
216
+ │ └── set-raw.ts # Set raw config values
217
+ └── agents/
218
+ ├── claude.ts # Claude Code config builder + reader
219
+ ├── codex.ts # Codex config builder + reader
220
+ ├── gemini.ts # Gemini CLI config builder + reader
221
+ └── opencode.ts # OpenCode config builder + reader
222
+ ```
234
223
 
235
224
  ## Agent Rule
236
225
 
@@ -243,15 +232,9 @@ Run `npx -y axconfig --help` to learn available options.
243
232
 
244
233
  Use `axconfig` to manage AI agent configurations with unified permission syntax.
245
234
  It translates `--allow` and `--deny` rules to agent-specific formats (Claude Code,
246
- Codex, Gemini CLI, OpenCode) and handles auth preservation automatically.
235
+ Codex, Gemini CLI, OpenCode).
247
236
  ```
248
237
 
249
- ## Development Status
250
-
251
- 🚧 **Work in Progress**
252
-
253
- Migrating config logic from axrun to axconfig.
254
-
255
238
  ## License
256
239
 
257
240
  MIT
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Claude Code ConfigReader.
3
+ *
4
+ * Reads and writes Claude Code configuration from settings.json.
5
+ */
6
+ import type { ConfigReader } from "../types.js";
7
+ /** Claude Code ConfigReader */
8
+ declare const claudeCodeConfigReader: ConfigReader;
9
+ export { claudeCodeConfigReader };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Claude Code ConfigReader.
3
+ *
4
+ * Reads and writes Claude Code configuration from settings.json.
5
+ */
6
+ import { mkdirSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { getAgentPathInfo } from "axshared";
9
+ import { atomicWriteFileSync } from "../atomic-write.js";
10
+ import { registerConfigReader } from "../reader.js";
11
+ import { createJsonConfigOperations, readJsonConfig, } from "../read-write-json-config.js";
12
+ /** Reverse mapping from Claude Code tool names to canonical names */
13
+ const REVERSE_TOOL_MAP = {
14
+ Read: "read",
15
+ Write: "write",
16
+ Edit: "edit",
17
+ Bash: "bash",
18
+ Glob: "glob",
19
+ Grep: "grep",
20
+ WebFetch: "web",
21
+ WebSearch: "web",
22
+ };
23
+ /** Path-restricted tools (lowercase) */
24
+ const PATH_TOOLS = new Set(["read", "write", "edit"]);
25
+ /**
26
+ * Get the settings.json path for a config directory.
27
+ */
28
+ function getSettingsPath(configDirectory) {
29
+ return path.join(configDirectory, "settings.json");
30
+ }
31
+ /**
32
+ * Read and parse settings.json, returning empty object if not found.
33
+ */
34
+ function readSettings(configDirectory) {
35
+ return readJsonConfig(getSettingsPath(configDirectory));
36
+ }
37
+ // Create shared raw config operations
38
+ const { readRaw, writeRaw, deleteRaw } = createJsonConfigOperations(getSettingsPath, "Claude Code");
39
+ /**
40
+ * Parse a Claude Code rule string back to unified format.
41
+ */
42
+ function parseClaudeRule(rule) {
43
+ // Check for pattern format: "Tool(pattern)"
44
+ const parenIndex = rule.indexOf("(");
45
+ if (parenIndex > 0 && rule.endsWith(")")) {
46
+ const toolName = rule.slice(0, parenIndex);
47
+ const pattern = rule.slice(parenIndex + 1, -1);
48
+ // Bash pattern: "Bash(git:*)" → pattern is "git:*", we want "git"
49
+ if (toolName === "Bash") {
50
+ const bashPattern = pattern.endsWith(":*")
51
+ ? pattern.slice(0, -2)
52
+ : pattern;
53
+ return { type: "bash", pattern: bashPattern };
54
+ }
55
+ // Path pattern: "Read(src/**)" → tool is "read", pattern is "src/**"
56
+ const canonicalTool = REVERSE_TOOL_MAP[toolName];
57
+ if (canonicalTool && PATH_TOOLS.has(canonicalTool)) {
58
+ return {
59
+ type: "path",
60
+ tool: canonicalTool,
61
+ pattern,
62
+ };
63
+ }
64
+ return undefined;
65
+ }
66
+ // Simple tool name: "Read" → { type: "tool", name: "read" }
67
+ const canonicalTool = REVERSE_TOOL_MAP[rule];
68
+ if (canonicalTool) {
69
+ return { type: "tool", name: canonicalTool };
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Read permissions from Claude Code config.
75
+ */
76
+ function readPermissions(configDirectory) {
77
+ try {
78
+ const settings = readSettings(configDirectory);
79
+ const permissions = settings.permissions;
80
+ if (!permissions) {
81
+ return { ok: true, value: undefined };
82
+ }
83
+ const allowRules = (permissions.allow ?? [])
84
+ .map((rule) => parseClaudeRule(rule))
85
+ .filter((rule) => rule !== undefined);
86
+ const denyRules = (permissions.deny ?? [])
87
+ .map((rule) => parseClaudeRule(rule))
88
+ .filter((rule) => rule !== undefined);
89
+ const config = {
90
+ allow: allowRules,
91
+ deny: denyRules,
92
+ };
93
+ return { ok: true, value: config };
94
+ }
95
+ catch (error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ return {
98
+ ok: false,
99
+ error: `Failed to read Claude Code config: ${message}`,
100
+ };
101
+ }
102
+ }
103
+ /**
104
+ * Read model from Claude Code config.
105
+ * Returns undefined when no model is explicitly configured.
106
+ */
107
+ function readModel(configDirectory) {
108
+ try {
109
+ const settings = readSettings(configDirectory);
110
+ const model = settings.model;
111
+ if (model === undefined || model === null) {
112
+ return { ok: true, value: undefined };
113
+ }
114
+ if (typeof model !== "string") {
115
+ return { ok: false, error: "Model value is not a string" };
116
+ }
117
+ return { ok: true, value: model };
118
+ }
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ return {
122
+ ok: false,
123
+ error: `Failed to read Claude Code model: ${message}`,
124
+ };
125
+ }
126
+ }
127
+ /**
128
+ * Write model to Claude Code config.
129
+ */
130
+ function writeModel(configDirectory, model) {
131
+ try {
132
+ mkdirSync(configDirectory, { recursive: true });
133
+ const settings = readSettings(configDirectory);
134
+ settings.model = model;
135
+ atomicWriteFileSync(getSettingsPath(configDirectory), JSON.stringify(settings, undefined, 2));
136
+ return { ok: true };
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error);
140
+ return {
141
+ ok: false,
142
+ error: `Failed to write Claude Code model: ${message}`,
143
+ };
144
+ }
145
+ }
146
+ // Get path info from axshared (single source of truth)
147
+ const pathInfo = getAgentPathInfo("claude");
148
+ /** Claude Code ConfigReader */
149
+ const claudeCodeConfigReader = {
150
+ agentId: "claude",
151
+ defaultConfigDir: pathInfo.defaultConfigDir,
152
+ envVar: pathInfo.envVar,
153
+ subdirectory: pathInfo.subdirectory,
154
+ buildRuntimeEnvironment: pathInfo.buildRuntimeEnvironment,
155
+ readPermissions,
156
+ readModel,
157
+ writeModel,
158
+ readRaw,
159
+ writeRaw,
160
+ deleteRaw,
161
+ };
162
+ // Self-register on import
163
+ registerConfigReader(claudeCodeConfigReader);
164
+ export { claudeCodeConfigReader };
@@ -9,6 +9,7 @@
9
9
  * - Path patterns like "Read(src/**)"
10
10
  */
11
11
  import type { ConfigBuilder } from "../types.js";
12
+ export { claudeCodeConfigReader } from "./claude-reader.js";
12
13
  /** Claude Code ConfigBuilder */
13
14
  declare const claudeCodeConfigBuilder: ConfigBuilder;
14
15
  export { claudeCodeConfigBuilder };
@@ -8,9 +8,12 @@
8
8
  * - Bash patterns like "Bash(git:*)"
9
9
  * - Path patterns like "Read(src/**)"
10
10
  */
11
- import { mkdirSync, writeFileSync } from "node:fs";
11
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
12
12
  import path from "node:path";
13
+ import { atomicWriteFileSync } from "../atomic-write.js";
13
14
  import { registerConfigBuilder } from "../builder.js";
15
+ // Re-export reader
16
+ export { claudeCodeConfigReader } from "./claude-reader.js";
14
17
  /** Claude Code tool name mapping */
15
18
  const TOOL_MAP = {
16
19
  read: "Read",
@@ -47,23 +50,45 @@ function translateRule(rule) {
47
50
  }
48
51
  }
49
52
  }
53
+ /**
54
+ * Read existing settings.json, returning empty object if not found.
55
+ * Throws if file exists but contains invalid JSON to prevent data loss.
56
+ */
57
+ function readExistingSettings(settingsPath) {
58
+ if (!existsSync(settingsPath)) {
59
+ return {};
60
+ }
61
+ try {
62
+ const content = readFileSync(settingsPath, "utf8");
63
+ return JSON.parse(content);
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ throw new Error(`Failed to parse existing settings at ${settingsPath}: ${message}`);
68
+ }
69
+ }
50
70
  /**
51
71
  * Build Claude Code configuration.
72
+ *
73
+ * Preserves existing settings (model, env, etc.) and only updates permissions.
52
74
  */
53
75
  function build(config, output) {
54
76
  mkdirSync(output, { recursive: true });
55
77
  const warnings = [];
56
78
  const permissions = config.permissions;
57
79
  const settingsPath = path.join(output, "settings.json");
80
+ // Read existing settings to preserve non-permission fields
81
+ const existingSettings = readExistingSettings(settingsPath);
58
82
  // If no permissions specified, deny all
59
83
  if (!permissions) {
60
84
  const settings = {
85
+ ...existingSettings,
61
86
  permissions: {
62
87
  allow: [],
63
88
  deny: [],
64
89
  },
65
90
  };
66
- writeFileSync(settingsPath, JSON.stringify(settings, undefined, 2));
91
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, undefined, 2));
67
92
  return {
68
93
  ok: true,
69
94
  env: { CLAUDE_CONFIG_DIR: output },
@@ -75,12 +100,13 @@ function build(config, output) {
75
100
  const allowRules = permissions.allow.map((rule) => translateRule(rule));
76
101
  const denyRules = permissions.deny.map((rule) => translateRule(rule));
77
102
  const settings = {
103
+ ...existingSettings,
78
104
  permissions: {
79
105
  allow: allowRules,
80
106
  deny: denyRules,
81
107
  },
82
108
  };
83
- writeFileSync(settingsPath, JSON.stringify(settings, undefined, 2));
109
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, undefined, 2));
84
110
  return {
85
111
  ok: true,
86
112
  env: { CLAUDE_CONFIG_DIR: output },
@@ -89,7 +115,7 @@ function build(config, output) {
89
115
  }
90
116
  /** Claude Code ConfigBuilder */
91
117
  const claudeCodeConfigBuilder = {
92
- agentId: "claude-code",
118
+ agentId: "claude",
93
119
  capabilities: CAPABILITIES,
94
120
  build,
95
121
  };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Codex ConfigReader.
3
+ *
4
+ * Reads and writes Codex configuration from config.toml and .rules files.
5
+ */
6
+ import type { ConfigReader } from "../types.js";
7
+ /** Codex ConfigReader */
8
+ declare const codexConfigReader: ConfigReader;
9
+ export { codexConfigReader };