envrcctl 0.0.3__tar.gz → 0.2.0__tar.gz

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 (27) hide show
  1. {envrcctl-0.0.3 → envrcctl-0.2.0}/PKG-INFO +131 -8
  2. envrcctl-0.2.0/README.md +363 -0
  3. {envrcctl-0.0.3 → envrcctl-0.2.0}/pyproject.toml +5 -5
  4. envrcctl-0.2.0/scripts/build_macos_auth_helper.sh +43 -0
  5. envrcctl-0.2.0/scripts/generate_completions.py +35 -0
  6. envrcctl-0.2.0/scripts/macos/envrcctl-macos-auth.swift +340 -0
  7. envrcctl-0.2.0/src/envrcctl/audit.py +470 -0
  8. envrcctl-0.2.0/src/envrcctl/auth.py +64 -0
  9. envrcctl-0.2.0/src/envrcctl/cli.py +1021 -0
  10. envrcctl-0.2.0/src/envrcctl/envrcctl-macos-auth +0 -0
  11. envrcctl-0.2.0/src/envrcctl/keychain.py +209 -0
  12. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/secrets.py +9 -1
  13. envrcctl-0.0.3/README.md +0 -244
  14. envrcctl-0.0.3/src/envrcctl/cli.py +0 -600
  15. envrcctl-0.0.3/src/envrcctl/keychain.py +0 -64
  16. {envrcctl-0.0.3 → envrcctl-0.2.0}/.gitignore +0 -0
  17. {envrcctl-0.0.3 → envrcctl-0.2.0}/LICENSE +0 -0
  18. {envrcctl-0.0.3 → envrcctl-0.2.0}/completions/envrcctl.bash +0 -0
  19. {envrcctl-0.0.3 → envrcctl-0.2.0}/completions/envrcctl.fish +0 -0
  20. {envrcctl-0.0.3 → envrcctl-0.2.0}/completions/envrcctl.zsh +0 -0
  21. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/__init__.py +0 -0
  22. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/command_runner.py +0 -0
  23. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/envrc.py +0 -0
  24. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/errors.py +0 -0
  25. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/main.py +0 -0
  26. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/managed_block.py +0 -0
  27. {envrcctl-0.0.3 → envrcctl-0.2.0}/src/envrcctl/secretservice.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: envrcctl
3
- Version: 0.0.3
3
+ Version: 0.2.0
4
4
  Summary: Manage .envrc with managed blocks and OS-backed secrets.
5
5
  License: MIT License
6
6
 
@@ -26,6 +26,10 @@ License: MIT License
26
26
  License-File: LICENSE
27
27
  Requires-Python: >=3.14
28
28
  Requires-Dist: typer>=0.24.1
29
+ Provides-Extra: test
30
+ Requires-Dist: bandit>=1.7.10; extra == 'test'
31
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
32
+ Requires-Dist: pytest>=9.0.2; extra == 'test'
29
33
  Description-Content-Type: text/markdown
30
34
 
31
35
  # envrcctl
@@ -41,10 +45,12 @@ It is designed for macOS first, with Linux support via SecretService.
41
45
  - Non-secret environment variables (CRUD)
42
46
  - Secrets stored in Keychain (macOS) or SecretService (Linux)
43
47
  - Inheritance control (`source_up` on/off)
44
- - Exec-based secret injection (`envrcctl exec -- ...`)
45
- - Secret injection for direnv (`eval "$(envrcctl inject)"`, TTY-guarded)
48
+ - Exec-based secret injection (`envrcctl exec -- ...`, TTY-guarded on Linux, TTY + macOS auth on macOS)
49
+ - Secret injection for direnv (`eval "$(envrcctl inject)"`, TTY-guarded on Linux, TTY + macOS auth on macOS)
46
50
  - Secret kinds (runtime/admin), with exec injecting runtime only
47
- - Secret get with clipboard default and TTY guard
51
+ - Secret get with clipboard default and TTY guard on Linux, plus macOS auth on macOS
52
+ - On macOS, `inject` and `exec` retrieve multiple runtime secrets with a single device owner authentication prompt
53
+ - Tamper-evident local audit log for secret access events
48
54
  - Diagnostics and migration helpers
49
55
  - Shell completion scripts
50
56
 
@@ -53,6 +59,7 @@ It is designed for macOS first, with Linux support via SecretService.
53
59
  - Python 3.14+
54
60
  - `direnv`
55
61
  - macOS Keychain (built-in) or Linux SecretService (`secret-tool`)
62
+ - device owner authentication (TouchID or Apple Watch)
56
63
 
57
64
  ## Installation
58
65
 
@@ -94,6 +101,45 @@ uv sync
94
101
  uv run python -m envrcctl.main --help
95
102
  ```
96
103
 
104
+ ### Build the macOS auth helper (macOS only)
105
+
106
+ The macOS device owner authentication flow requires a native helper named
107
+ `envrcctl-macos-auth`.
108
+
109
+ Build it and place the binary at either:
110
+
111
+ - `src/envrcctl/envrcctl-macos-auth`
112
+ - or a custom path set via `ENVRCCTL_MACOS_AUTH_HELPER`
113
+
114
+ Example build flow:
115
+
116
+ ```sh
117
+ swiftc -O -framework LocalAuthentication -framework Security \
118
+ scripts/macos/envrcctl-macos-auth.swift \
119
+ -o src/envrcctl/envrcctl-macos-auth
120
+ chmod +x src/envrcctl/envrcctl-macos-auth
121
+ ```
122
+
123
+ You can also use the repository build script:
124
+
125
+ ```sh
126
+ sh scripts/build_macos_auth_helper.sh
127
+ ```
128
+
129
+ If you want to write the helper to a custom location, pass the source and output paths explicitly:
130
+
131
+ ```sh
132
+ sh scripts/build_macos_auth_helper.sh \
133
+ scripts/macos/envrcctl-macos-auth.swift \
134
+ /usr/local/bin/envrcctl-macos-auth
135
+ ```
136
+
137
+ If you install the helper elsewhere, set:
138
+
139
+ ```sh
140
+ export ENVRCCTL_MACOS_AUTH_HELPER=/path/to/envrcctl-macos-auth
141
+ ```
142
+
97
143
  ## Quick Start
98
144
 
99
145
  1. Initialize a managed block in `.envrc`:
@@ -160,6 +206,13 @@ envrcctl secret get OPENAI_API_KEY
160
206
  envrcctl secret get OPENAI_API_KEY --plain
161
207
  ```
162
208
 
209
+ `envrcctl secret get` behavior is platform-specific:
210
+
211
+ - On Linux, the current TTY-based behavior remains in place.
212
+ - On macOS, `secret get` requires the existing interactive-shell check and successful macOS device owner authentication before revealing or copying the secret.
213
+
214
+ In practice, macOS authentication may use Touch ID and, when supported by your system configuration, Apple Watch approval or password fallback.
215
+
163
216
  For CI-safe input:
164
217
 
165
218
  ```sh
@@ -175,13 +228,33 @@ envrcctl exec -k OPENAI_API_KEY -- python script.py
175
228
 
176
229
  Exec injects runtime secrets only.
177
230
 
231
+ `envrcctl exec` behavior is platform-specific:
232
+
233
+ - On Linux, the current behavior remains in place.
234
+ - On macOS, `exec` requires the existing interactive-shell check and successful macOS device owner authentication before runtime secrets are injected into the child process.
235
+ - When multiple runtime secrets are selected, macOS performs a single authentication step and then retrieves all requested secrets in one helper call.
236
+
237
+ In practice, macOS authentication may use Touch ID and, when supported by your system configuration, Apple Watch approval or password fallback.
238
+
178
239
  ### Inject secrets for direnv
179
240
 
180
241
  ```sh
181
242
  envrcctl inject
182
243
  ```
183
244
 
184
- Non-interactive runs are blocked unless `--force` is provided.
245
+ Linux keeps the current behavior: non-interactive runs are blocked unless `--force` is provided.
246
+
247
+ On macOS, `envrcctl inject` requires both:
248
+ - the existing interactive-shell check
249
+ - successful macOS device owner authentication
250
+
251
+ When multiple runtime secrets are present, `inject` performs one authentication step and retrieves all eligible secrets in a single bulk helper request.
252
+
253
+ That authentication is expected to be satisfied through macOS mechanisms such as Touch ID and, when your system offers it, Apple Watch approval or password fallback.
254
+
255
+ If the native helper is missing or not executable, `inject` fails closed with an
256
+ authentication-helper error. Build or install `envrcctl-macos-auth` before using
257
+ macOS-authenticated secret commands.
185
258
 
186
259
  ### Effective view (masked)
187
260
 
@@ -189,12 +262,51 @@ Non-interactive runs are blocked unless `--force` is provided.
189
262
  envrcctl eval
190
263
  ```
191
264
 
265
+ ### Audit log
266
+
267
+ ```sh
268
+ envrcctl audit list
269
+ envrcctl audit show --index 0
270
+ envrcctl audit verify
271
+ ```
272
+
273
+ `envrcctl` records tamper-evident local audit events for:
274
+
275
+ - `secret get`
276
+ - `inject`
277
+ - `exec`
278
+
279
+ The audit log:
280
+
281
+ - never stores plaintext secret values
282
+ - stores variable names, secret ref metadata, working directory, and `exec` command metadata
283
+ - chains events with `prev_hash` and `hash` so silent modification or deletion is detectable
284
+
285
+ Default audit log storage locations:
286
+
287
+ - macOS: `~/Library/Application Support/envrcctl/audit/`
288
+ - Linux: `$XDG_STATE_HOME/envrcctl/audit/` when `XDG_STATE_HOME` is set
289
+ - Linux fallback: `~/.local/state/envrcctl/audit/`
290
+
291
+ The audit store currently uses:
292
+
293
+ - `audit.jsonl` for append-only event records
294
+ - `latest_hash` for the latest chain hash
295
+ - `meta.json` for metadata
296
+
297
+ `envrcctl audit verify` checks the hash chain and reports failures if audit records appear to have been modified.
298
+
192
299
  ### Diagnostics
193
300
 
194
301
  ```sh
195
302
  envrcctl doctor
196
303
  ```
197
304
 
305
+ `doctor` also checks audit health and warns when:
306
+
307
+ - the audit chain does not verify
308
+ - the audit store permissions are insecure
309
+
198
310
  ### Migration
199
311
 
200
312
  ```sh
@@ -253,8 +365,19 @@ uv run python scripts/generate_completions.py
253
365
  - Secrets are never written to `.envrc`
254
366
  - Secrets are never passed in CLI arguments
255
367
  - `.envrc` updates are atomic
256
- - `inject` is blocked in non-interactive environments unless `--force` is provided
257
- - `secret get` is clipboard-only by default; plaintext output is TTY-guarded
368
+ - On Linux, `inject` is blocked in non-interactive environments unless `--force` is provided
369
+ - On macOS, `inject` requires both the interactive-shell check and successful device owner authentication
370
+ - On Linux, `secret get` is clipboard-only by default and plaintext output is TTY-guarded
371
+ - On macOS, `secret get` requires both the interactive-shell check and successful device owner authentication
372
+ - On Linux, `exec` keeps the current behavior
373
+ - On macOS, `exec` requires both the interactive-shell check and successful device owner authentication before runtime secrets are injected into the child process
374
+ - On macOS, authentication is mediated by the OS and may use Touch ID, Apple Watch approval, or password fallback depending on system support and configuration
375
+ - On macOS, authenticated commands require the native helper `envrcctl-macos-auth`
376
+ - The helper is discovered from `ENVRCCTL_MACOS_AUTH_HELPER` or `src/envrcctl/envrcctl-macos-auth`
377
+ - If the helper is missing, invalid, or not executable, macOS secret-accessing commands fail closed
378
+ - Secret-access actions are recorded in a local tamper-evident audit log
379
+ - Audit records never include plaintext secret values
380
+ - Audit integrity is based on a hash chain and can be checked with `envrcctl audit verify`
258
381
  - The tool refuses to write to world-writable `.envrc`
259
382
 
260
383
  ## Development
@@ -271,4 +394,4 @@ Based on the article below, I added commands such as `exec`. Thank you for the h
271
394
 
272
395
  ## License
273
396
 
274
- MIT
397
+ MIT
@@ -0,0 +1,363 @@
1
+ # envrcctl
2
+
3
+ envrcctl is a CLI tool that manages `.envrc` files safely through a managed
4
+ block, with secrets stored in your OS key store instead of the file.
5
+
6
+ It is designed for macOS first, with Linux support via SecretService.
7
+
8
+ ## Features
9
+
10
+ - Safe, structured edits to `.envrc` (managed block only)
11
+ - Non-secret environment variables (CRUD)
12
+ - Secrets stored in Keychain (macOS) or SecretService (Linux)
13
+ - Inheritance control (`source_up` on/off)
14
+ - Exec-based secret injection (`envrcctl exec -- ...`, TTY-guarded on Linux, TTY + macOS auth on macOS)
15
+ - Secret injection for direnv (`eval "$(envrcctl inject)"`, TTY-guarded on Linux, TTY + macOS auth on macOS)
16
+ - Secret kinds (runtime/admin), with exec injecting runtime only
17
+ - Secret get with clipboard default and TTY guard on Linux, plus macOS auth on macOS
18
+ - On macOS, `inject` and `exec` retrieve multiple runtime secrets with a single device owner authentication prompt
19
+ - Tamper-evident local audit log for secret access events
20
+ - Diagnostics and migration helpers
21
+ - Shell completion scripts
22
+
23
+ ## Requirements
24
+
25
+ - Python 3.14+
26
+ - `direnv`
27
+ - macOS Keychain (built-in) or Linux SecretService (`secret-tool`)
28
+ - device owner authentication (TouchID or Apple Watch)
29
+
30
+ ## Installation
31
+
32
+ ### macOS (Homebrew)
33
+
34
+ Tap and install:
35
+
36
+ ```sh
37
+ brew tap rioriost/envrcctl
38
+ brew install envrcctl
39
+ ```
40
+
41
+ After release, Homebrew will download the release from GitHub.
42
+
43
+ Install direnv with Homebrew:
44
+
45
+ ```sh
46
+ brew install direnv
47
+ ```
48
+
49
+ ### Linux (pipx, recommended)
50
+
51
+ ```sh
52
+ pipx install envrcctl
53
+ ```
54
+
55
+ ### Linux (uv)
56
+
57
+ ```sh
58
+ uv tool install envrcctl
59
+ ```
60
+
61
+ ### From source (macOS/Linux)
62
+
63
+ ```sh
64
+ git clone <REPO_URL>
65
+ cd envrcctl
66
+ uv sync
67
+ uv run python -m envrcctl.main --help
68
+ ```
69
+
70
+ ### Build the macOS auth helper (macOS only)
71
+
72
+ The macOS device owner authentication flow requires a native helper named
73
+ `envrcctl-macos-auth`.
74
+
75
+ Build it and place the binary at either:
76
+
77
+ - `src/envrcctl/envrcctl-macos-auth`
78
+ - or a custom path set via `ENVRCCTL_MACOS_AUTH_HELPER`
79
+
80
+ Example build flow:
81
+
82
+ ```sh
83
+ swiftc -O -framework LocalAuthentication -framework Security \
84
+ scripts/macos/envrcctl-macos-auth.swift \
85
+ -o src/envrcctl/envrcctl-macos-auth
86
+ chmod +x src/envrcctl/envrcctl-macos-auth
87
+ ```
88
+
89
+ You can also use the repository build script:
90
+
91
+ ```sh
92
+ sh scripts/build_macos_auth_helper.sh
93
+ ```
94
+
95
+ If you want to write the helper to a custom location, pass the source and output paths explicitly:
96
+
97
+ ```sh
98
+ sh scripts/build_macos_auth_helper.sh \
99
+ scripts/macos/envrcctl-macos-auth.swift \
100
+ /usr/local/bin/envrcctl-macos-auth
101
+ ```
102
+
103
+ If you install the helper elsewhere, set:
104
+
105
+ ```sh
106
+ export ENVRCCTL_MACOS_AUTH_HELPER=/path/to/envrcctl-macos-auth
107
+ ```
108
+
109
+ ## Quick Start
110
+
111
+ 1. Initialize a managed block in `.envrc`:
112
+
113
+ ```sh
114
+ envrcctl init
115
+ ```
116
+
117
+ If `.envrc` already exists, you'll be prompted to confirm. Use `--yes` to skip the prompt in non-interactive runs. Add `--inject` to explicitly insert the inject line.
118
+
119
+ 2. Add non-secret variables:
120
+
121
+ ```sh
122
+ envrcctl set FOO bar
123
+ envrcctl get FOO
124
+ envrcctl list
125
+ ```
126
+
127
+ 3. Enable inheritance:
128
+
129
+ ```sh
130
+ envrcctl inherit on
131
+ ```
132
+
133
+ 4. Store a secret:
134
+
135
+ ```sh
136
+ envrcctl secret set OPENAI_API_KEY --account openai:prod
137
+ ```
138
+
139
+ 5. Add the inject line explicitly:
140
+
141
+ ```sh
142
+ envrcctl init --inject
143
+ ```
144
+
145
+ This inserts `eval "$(envrcctl inject)"` into the managed block.
146
+
147
+ 6. Allow direnv:
148
+
149
+ ```sh
150
+ direnv allow
151
+ ```
152
+
153
+ ## Commands
154
+
155
+ ### Non-secret variables
156
+
157
+ ```sh
158
+ envrcctl set VAR value
159
+ envrcctl unset VAR
160
+ envrcctl get VAR
161
+ envrcctl list
162
+ ```
163
+
164
+ ### Secrets
165
+
166
+ ```sh
167
+ envrcctl secret set OPENAI_API_KEY --account openai:prod --kind runtime
168
+ envrcctl secret set OPENAI_API_KEY --account openai:admin --kind admin
169
+ envrcctl secret unset OPENAI_API_KEY
170
+ envrcctl secret list
171
+ envrcctl secret get OPENAI_API_KEY
172
+ envrcctl secret get OPENAI_API_KEY --plain
173
+ ```
174
+
175
+ `envrcctl secret get` behavior is platform-specific:
176
+
177
+ - On Linux, the current TTY-based behavior remains in place.
178
+ - On macOS, `secret get` requires the existing interactive-shell check and successful macOS device owner authentication before revealing or copying the secret.
179
+
180
+ In practice, macOS authentication may use Touch ID and, when supported by your system configuration, Apple Watch approval or password fallback.
181
+
182
+ For CI-safe input:
183
+
184
+ ```sh
185
+ echo -n "$OPENAI_API_KEY" | envrcctl secret set OPENAI_API_KEY --account openai:prod --stdin
186
+ ```
187
+
188
+ ### Exec secrets without stdout
189
+
190
+ ```sh
191
+ envrcctl exec -- python script.py
192
+ envrcctl exec -k OPENAI_API_KEY -- python script.py
193
+ ```
194
+
195
+ Exec injects runtime secrets only.
196
+
197
+ `envrcctl exec` behavior is platform-specific:
198
+
199
+ - On Linux, the current behavior remains in place.
200
+ - On macOS, `exec` requires the existing interactive-shell check and successful macOS device owner authentication before runtime secrets are injected into the child process.
201
+ - When multiple runtime secrets are selected, macOS performs a single authentication step and then retrieves all requested secrets in one helper call.
202
+
203
+ In practice, macOS authentication may use Touch ID and, when supported by your system configuration, Apple Watch approval or password fallback.
204
+
205
+ ### Inject secrets for direnv
206
+
207
+ ```sh
208
+ envrcctl inject
209
+ ```
210
+
211
+ Linux keeps the current behavior: non-interactive runs are blocked unless `--force` is provided.
212
+
213
+ On macOS, `envrcctl inject` requires both:
214
+ - the existing interactive-shell check
215
+ - successful macOS device owner authentication
216
+
217
+ When multiple runtime secrets are present, `inject` performs one authentication step and retrieves all eligible secrets in a single bulk helper request.
218
+
219
+ That authentication is expected to be satisfied through macOS mechanisms such as Touch ID and, when your system offers it, Apple Watch approval or password fallback.
220
+
221
+ If the native helper is missing or not executable, `inject` fails closed with an
222
+ authentication-helper error. Build or install `envrcctl-macos-auth` before using
223
+ macOS-authenticated secret commands.
224
+
225
+ ### Effective view (masked)
226
+
227
+ ```sh
228
+ envrcctl eval
229
+ ```
230
+
231
+ ### Audit log
232
+
233
+ ```sh
234
+ envrcctl audit list
235
+ envrcctl audit show --index 0
236
+ envrcctl audit verify
237
+ ```
238
+
239
+ `envrcctl` records tamper-evident local audit events for:
240
+
241
+ - `secret get`
242
+ - `inject`
243
+ - `exec`
244
+
245
+ The audit log:
246
+
247
+ - never stores plaintext secret values
248
+ - stores variable names, secret ref metadata, working directory, and `exec` command metadata
249
+ - chains events with `prev_hash` and `hash` so silent modification or deletion is detectable
250
+
251
+ Default audit log storage locations:
252
+
253
+ - macOS: `~/Library/Application Support/envrcctl/audit/`
254
+ - Linux: `$XDG_STATE_HOME/envrcctl/audit/` when `XDG_STATE_HOME` is set
255
+ - Linux fallback: `~/.local/state/envrcctl/audit/`
256
+
257
+ The audit store currently uses:
258
+
259
+ - `audit.jsonl` for append-only event records
260
+ - `latest_hash` for the latest chain hash
261
+ - `meta.json` for metadata
262
+
263
+ `envrcctl audit verify` checks the hash chain and reports failures if audit records appear to have been modified.
264
+
265
+ ### Diagnostics
266
+
267
+ ```sh
268
+ envrcctl doctor
269
+ ```
270
+
271
+ `doctor` also checks audit health and warns when:
272
+
273
+ - the audit chain does not verify
274
+ - the audit store permissions are insecure
275
+
276
+ ### Migration
277
+
278
+ ```sh
279
+ envrcctl migrate
280
+ ```
281
+
282
+ You'll be prompted when unmanaged exports or secret refs are detected. Use `--yes` to confirm in non-interactive runs.
283
+
284
+ ## Backend Selection (macOS/Linux)
285
+
286
+ envrcctl selects a backend automatically by platform, or via `ENVRCCTL_BACKEND`.
287
+
288
+ Supported schemes:
289
+
290
+ - `kc` — macOS Keychain
291
+ - `ss` — SecretService via `secret-tool`
292
+
293
+ Example:
294
+
295
+ ```sh
296
+ ENVRCCTL_BACKEND=ss envrcctl secret set OPENAI_API_KEY --account openai:prod
297
+ ```
298
+
299
+ Secret references are stored as:
300
+
301
+ ```
302
+ <scheme>:<service>:<account>:<kind>
303
+ ```
304
+
305
+ `kind` is `runtime` or `admin` (default: `runtime`).
306
+
307
+ Example:
308
+
309
+ ```
310
+ kc:st.rio.envrcctl:openai:prod:runtime
311
+ kc:st.rio.envrcctl:openai:admin:admin
312
+ ```
313
+
314
+ ## Shell Completion
315
+
316
+ ```sh
317
+ envrcctl --install-completion
318
+ envrcctl --show-completion bash
319
+ envrcctl --show-completion zsh
320
+ envrcctl --show-completion fish
321
+ ```
322
+
323
+ Generated scripts are stored under `completions/`. To refresh:
324
+
325
+ ```sh
326
+ uv run python scripts/generate_completions.py
327
+ ```
328
+
329
+ ## Security Notes
330
+
331
+ - Secrets are never written to `.envrc`
332
+ - Secrets are never passed in CLI arguments
333
+ - `.envrc` updates are atomic
334
+ - On Linux, `inject` is blocked in non-interactive environments unless `--force` is provided
335
+ - On macOS, `inject` requires both the interactive-shell check and successful device owner authentication
336
+ - On Linux, `secret get` is clipboard-only by default and plaintext output is TTY-guarded
337
+ - On macOS, `secret get` requires both the interactive-shell check and successful device owner authentication
338
+ - On Linux, `exec` keeps the current behavior
339
+ - On macOS, `exec` requires both the interactive-shell check and successful device owner authentication before runtime secrets are injected into the child process
340
+ - On macOS, authentication is mediated by the OS and may use Touch ID, Apple Watch approval, or password fallback depending on system support and configuration
341
+ - On macOS, authenticated commands require the native helper `envrcctl-macos-auth`
342
+ - The helper is discovered from `ENVRCCTL_MACOS_AUTH_HELPER` or `src/envrcctl/envrcctl-macos-auth`
343
+ - If the helper is missing, invalid, or not executable, macOS secret-accessing commands fail closed
344
+ - Secret-access actions are recorded in a local tamper-evident audit log
345
+ - Audit records never include plaintext secret values
346
+ - Audit integrity is based on a hash chain and can be checked with `envrcctl audit verify`
347
+ - The tool refuses to write to world-writable `.envrc`
348
+
349
+ ## Development
350
+
351
+ ```sh
352
+ uv sync
353
+ .venv/bin/envrcctl --help
354
+ ```
355
+
356
+ ## Acknowledgements
357
+
358
+ Based on the article below, I added commands such as `exec`. Thank you for the helpful hints.
359
+ “[もう.envにAPIキーを平文で置くのはやめた — macOS Keychain管理CLI「LLM Key Ring」](https://zenn.dev/yottayoshida/articles/llm-key-ring-secure-api-key-management)”
360
+
361
+ ## License
362
+
363
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "envrcctl"
3
- version = "0.0.3"
3
+ version = "0.2.0"
4
4
  description = "Manage .envrc with managed blocks and OS-backed secrets."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -18,13 +18,13 @@ build-backend = "hatchling.build"
18
18
 
19
19
  [tool.hatch.build.targets.wheel]
20
20
  packages = ["src/envrcctl"]
21
- include = ["completions/**"]
21
+ include = ["completions/**", "src/envrcctl/envrcctl-macos-auth"]
22
22
 
23
23
  [tool.hatch.build.targets.sdist]
24
- include = ["src/envrcctl/**", "completions/**", "README.md", "LICENSE", "pyproject.toml"]
24
+ include = ["src/envrcctl/**", "completions/**", "scripts/**", "README.md", "LICENSE", "pyproject.toml"]
25
25
 
26
- [dependency-groups]
27
- dev = [
26
+ [project.optional-dependencies]
27
+ test = [
28
28
  "pytest>=9.0.2",
29
29
  "pytest-cov>=7.0.0",
30
30
  "bandit>=1.7.10",
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
5
+ REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
6
+ SWIFT_SOURCE="${1:-$REPO_ROOT/scripts/macos/envrcctl-macos-auth.swift}"
7
+ OUTPUT_PATH="${2:-$REPO_ROOT/src/envrcctl/envrcctl-macos-auth}"
8
+
9
+ if [ "$(uname -s)" != "Darwin" ]; then
10
+ echo "This helper can only be built on macOS." >&2
11
+ exit 1
12
+ fi
13
+
14
+ if ! command -v swiftc >/dev/null 2>&1; then
15
+ echo "swiftc not found. Install Xcode Command Line Tools first." >&2
16
+ exit 1
17
+ fi
18
+
19
+ if [ ! -f "$SWIFT_SOURCE" ]; then
20
+ echo "Swift source not found: $SWIFT_SOURCE" >&2
21
+ echo "Pass the source path as the first argument or create scripts/macos/envrcctl-macos-auth.swift." >&2
22
+ exit 1
23
+ fi
24
+
25
+ mkdir -p "$(dirname "$OUTPUT_PATH")"
26
+
27
+ echo "Building macOS auth helper..."
28
+ echo " source: $SWIFT_SOURCE"
29
+ echo " output: $OUTPUT_PATH"
30
+
31
+ swiftc \
32
+ -O \
33
+ -framework LocalAuthentication \
34
+ -framework Security \
35
+ "$SWIFT_SOURCE" \
36
+ -o "$OUTPUT_PATH"
37
+
38
+ chmod 755 "$OUTPUT_PATH"
39
+
40
+ echo "Build complete: $OUTPUT_PATH"
41
+ echo
42
+ echo "You can override the helper path at runtime with:"
43
+ echo " ENVRCCTL_MACOS_AUTH_HELPER=$OUTPUT_PATH"
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from click.shell_completion import get_completion_class
6
+ from typer.main import get_command
7
+
8
+ from envrcctl.cli import app
9
+
10
+ SHELLS = ("bash", "zsh", "fish")
11
+
12
+
13
+ def main() -> None:
14
+ repo_root = Path(__file__).resolve().parents[1]
15
+ output_dir = repo_root / "completions"
16
+ output_dir.mkdir(parents=True, exist_ok=True)
17
+
18
+ command = get_command(app)
19
+ complete_var = "_ENVRCCTL_COMPLETE"
20
+
21
+ for shell in SHELLS:
22
+ comp_cls = get_completion_class(shell)
23
+ if comp_cls is None:
24
+ raise RuntimeError(f"Unsupported shell: {shell}")
25
+ comp = comp_cls(command, {}, "envrcctl", complete_var)
26
+ content = comp.source()
27
+ if not content.strip():
28
+ raise RuntimeError(f"Failed to generate {shell} completion.")
29
+ if not content.endswith("\n"):
30
+ content += "\n"
31
+ (output_dir / f"envrcctl.{shell}").write_text(content, encoding="utf-8")
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()