kagi-vault 0.1.6

Encrypted secrets and environment variable manager for teams — a secure, team-ready dotenv alternative with per-service isolation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
# kagi

[![skills.sh](https://skills.sh/b/BANG88/kagi)](https://skills.sh/BANG88/kagi)

![kagi README banner](docs/kagi-readme-banner.png)

A secure, team-ready CLI for managing encrypted secrets and environment variables — a drop-in replacement for `.env` files with per-service isolation and team sharing.

**kagi** keeps your secrets encrypted at rest using XChaCha20-Poly1305 while making them easy to inject into applications during development and deployment.

---

## Features

- Encrypted secrets at rest with XChaCha20-Poly1305.
- Team-ready by default: one developer is just one member.
- Service and environment scopes like `api/development` and `web/production`.
- `development` is the default environment, so daily commands stay short.
- Nested service inference lets `kagi run bun dev` work inside `./api` and mapped monorepo packages like `./apps/api`.
- `.kagi/` is designed to be committed; private keys stay on each device.
- `get --show` and `export` require terminal confirmation before revealing values.
- Conservative `.env` migration in `kagi init` detects high-confidence `.env*` files and can map monorepo service folders.
- Encrypted small-file artifacts via `kagi file`, scoped the same way as env secrets.
- Shell completions for bash, zsh, fish, elvish, and powershell via `kagi completions`.

---

## Installation

### From crates.io

```bash
# Default: includes the remote sync server
cargo install kagi-vault

# CLI-only: excludes server code and server-related commands
cargo install kagi-vault --no-default-features
```

Requires Rust 1.85+ (2024 edition).

### From a local checkout

```bash
git clone https://github.com/BANG88/kagi.git
cd kagi

# Default: includes the remote sync server
cargo install --path .

# CLI-only: excludes server code and server-related commands
cargo install --path . --no-default-features
```

### As a library

Individual crates can be added as dependencies:

```bash
# Core domain types and traits
cargo add kagi-domain

# Encryption (XChaCha20-Poly1305)
cargo add kagi-crypto

# Local storage and key management
cargo add kagi-store

# Remote sync client
cargo add kagi-sync

# HTTP server (requires server feature)
cargo add kagi-server
```

### Optional Codex / OpenCode skill

This repository includes a skill for agents that help users operate kagi
projects safely. The skill files live in `skills/kagi/SKILL.md` and are
registered via `skills.sh.json` on the [skills.sh](https://skills.sh) directory.

#### Install with skills.sh

Use the [skills.sh](https://skills.sh) CLI (requires Node.js / npx):

```bash
npx skills add BANG88/kagi
```

This copies the skill into the agent's skills directory. To force reinstall:

```bash
npx skills add BANG88/kagi --force
```

---

## Daily Development

### 1. Initialize once

```bash
kagi init --nested --envs
```

`--envs` without a value creates the standard environments:
`development`, `test`, and `production`.

`development` is the default, so you usually do not type it. `--nested` lets
kagi infer the service from the folder you are in. In monorepos, `init` records
detected service paths such as `apps/api` and `packages/api` so commands inside
those folders do not need `--service`.

Commit the generated `.kagi/` files:

```bash
git add .kagi .gitignore
git commit -m "chore: initialize kagi"
```

Private keys are not written to `.kagi/`.

### 2. Set secrets

From the repository root:

```bash
kagi set api DATABASE_URL postgres://localhost/api
kagi set api production DATABASE_URL postgres://db/prod
```

Keys passed to `set` and `get` are normalized to upper snake case by default,
so `abc-d` becomes `ABC_D`. kagi prints a short tip when it normalizes a key.

Inside `./api`:

```bash
kagi set DATABASE_URL postgres://localhost/api
kagi set production DATABASE_URL postgres://db/prod
```

Both short commands write to the same scopes:

| Command | Scope |
|---------|-------|
| `kagi set api DATABASE_URL ...` | `api/development` |
| `kagi set DATABASE_URL ...` inside `./api` | `api/development` |
| `kagi set api production DATABASE_URL ...` | `api/production` |
| `kagi set production DATABASE_URL ...` inside `./api` | `api/production` |

### 3. Check what exists

```bash
kagi get
kagi get api
kagi get api production
```

`get` lists services, environments, and keys with masked values. Reveal values
only when you really need them:

```bash
kagi get api --show
kagi get api DATABASE_URL
```

Both commands require an interactive `y` confirmation.

### 4. Run your app

From the repository root:

```bash
kagi run api bun dev
kagi run api production bun start
```

Inside `./api`:

```bash
kagi run bun dev
kagi run production bun start
```

`kagi run` injects the selected environment variables into the child process.
For shell syntax such as pipes, redirects, or `$VAR` expansion, run a shell
explicitly:

```bash
kagi run api sh -c 'echo "$DATABASE_URL" | wc -c'
```

### 5. Commit encrypted changes

```bash
git add .kagi
git commit -m "chore: update kagi secrets"
```

Do not commit real `.env` files. `kagi init` updates `.gitignore` so `.env`,
`.env.*`, and local private material stay out of Git.

---

## Common Commands

| Task | Command |
|------|---------|
| Initialize with standard envs | `kagi init --nested --envs` |
| Set development secret | `kagi set api KEY value` |
| Set production secret | `kagi set api production KEY value` |
| List masked keys | `kagi get` |
| Reveal listed values | `kagi get api --show` |
| Search keys | `kagi search DATABASE` |
| Search including values | `kagi search --values localhost` |
| Show current project context | `kagi status` |
| Run app with development env | `kagi run api bun dev` |
| Run app from inside service folder | `kagi run bun dev` |
| Add an environment | `kagi env add staging` |
| Rename an environment | `kagi env rename staging preview` |
| Delete an environment | `kagi env remove preview` |
| List environments | `kagi env list` |
| List project members | `kagi member list` |
| Import an env file | `kagi import api --file .env.local` |
| Preview an import | `kagi import api --file .env.local --dry-run` |
| Import and normalize keys | `kagi import api --file .env.local --upper-snake` |
| Add an encrypted file | `kagi file add api service-account.json` |
| List encrypted files | `kagi file list api` |
| Restore an encrypted file | `kagi file restore api service-account.json` |
| Export all service envs | `kagi export api --out .` |
| Sync missing keys from example | `kagi sync --service api` |
| Generate shell completions | `kagi completions zsh` |

Use `--service <name>` when a shortcut would be ambiguous:

```bash
kagi set --service api production DATABASE_URL postgres://db/prod
kagi run --service api production bun start
```

Environment names cannot conflict with existing service names.

---

## Working With `.env` Files

`kagi init` detects high-confidence `.env*` files up to four levels deep and
offers to import them during initialization. It is intentionally conservative:
`.env` and `.env.example` map to the default environment, while files like
`.env.dev` are only imported automatically when `dev` is one of the configured
environments. Use `--no-migrate` to skip this import prompt.

When `--nested` is enabled, detected nested env files also create monorepo
service mappings. For example, `apps/api/.env.dev` maps commands run inside
`apps/api` to the `apps-api` service, while `packages/api/.env.dev` maps to
`packages-api`.

Import existing local files manually:

```bash
kagi import api --file .env.development
kagi import api production --file .env.production
kagi import api --file .env.local --dry-run
```

Manual imports preserve key names by default. Add `--upper-snake` if you want
imported keys normalized the same way as `set` and `get`.

Export creates normal runtime files when needed:

```bash
kagi export api --out .
```

That writes one file per environment:

```text
.env.development
.env.test
.env.production
```

Exporting decrypted values requires terminal confirmation. Prefer `kagi run`
for day-to-day scripts.

`sync` is useful when `.env.example` gains a new key:

```bash
kagi sync --service api
```

Existing values are never overwritten.

---

## Working With Encrypted Files

Use `kagi file` for small secret files that should share the same service and
environment scope as your env secrets:

```bash
kagi file add api service-account.json
kagi file list api
kagi file restore api service-account.json
```

Inside a nested or monorepo service directory, scope inference works the same as
`set`, `get`, `run`, and `import`:

```bash
cd apps/api
kagi file add dev service-account.json
kagi file list dev
kagi file restore dev service-account.json
```

The command stores encrypted blobs under `.kagi/files/`. File names, restore
paths, and scopes are stored in encrypted `files/index.enc`; Git and the remote
server only see opaque encrypted artifact ids such as `files/kgf_xxxxxx.enc`.

Restoring writes plaintext back to the repo-relative path captured at `add`
time, unless `--out <path>` is supplied:

```bash
kagi file restore api service-account.json --out apps/api/service-account.json
```

Safety limits:

- Default max file size is 1 MiB.
- `--allow-large` raises the limit to 5 MiB.
- Directories, symlinks, device files, build artifacts, `.git/`, and `.kagi/`
  paths are rejected.
- Git-tracked plaintext files are rejected; remove them from Git first with
  `git rm --cached <path>`.
- `show` requires terminal confirmation because it prints decrypted content.
- `restore` refuses to overwrite existing plaintext unless `--force` is used.

Git-backed and server-backed sharing use the same encrypted project state. Commit
or push `.kagi/files/**`; do not commit restored plaintext files.

---

## Team Flow

A project is always team-ready. If you work alone, you are the only member.

New device or teammate:

```bash
kagi member request --name alice
git add .kagi/access.json
git commit -m "chore: request kagi access"
```

An existing member approves:

```bash
kagi member list
kagi member approve <member_id>
git add .kagi/access.json
git commit -m "chore: approve kagi member"
```

If multiple people request access at the same time, keep all pending entries in
`.kagi/access.json` when merging their PRs.

Remove access:

```bash
kagi member remove <member_id>
git add .kagi
git commit -m "chore: remove kagi member"
```

`member remove` rotates the project key internally and re-encrypts current
secrets for active members. If rotation is interrupted, kagi writes a local
journal outside the repository and retries safely on the next command.

---

## Architecture Overview

### Without Server (Git-backed)

![Git-backed workflow](docs/diagram-git-backed.png)

Encrypted secrets are shared through Git commits and pulls. New members use `kagi member request` to request access, and existing members use `kagi member approve` to grant it.

### With Server (Remote Sync)

**1. Server initialization**

![Server init flow](docs/diagram-server-init.png)

**2. Team collaboration**

![Server team flow](docs/diagram-server-team.png)

- `.kagi/` stays local and out of Git.
- Project tokens contain the remote URL, project ID, and server fingerprint.

**Choose:**
- **Without Server**: commit `.kagi/` to Git (default, simple).
- **With Server**: keep `.kagi/` local, use remote sync (team control).

---

## Remote Server Sync

Git-backed `.kagi/` sharing is the default workflow. If a team does not want to
commit `.kagi/`, run a self-hosted Kagi server instead.

Status: **production-ready for self-hosted team use** — requires HTTPS, proper
backups, and monitoring. See `docs/remote-sync-server.md` for the full
protocol, deployment guide, and Docker/systemd examples.

The Kagi server is explicitly **single-tenant**. Each server instance serves one
team or organization. Do not run a single server instance for unrelated tenants
without additional isolation.

### HTTP restrictions

The server rejects non-localhost `http://` remotes by default. Use HTTPS for
any public or LAN deployment. For local development only, pass
`--allow-insecure-http` or set `KAGI_ALLOW_INSECURE_HTTP=1`:

```bash
kagi remote register --remote http://127.0.0.1:8787
```

### Start the server

```bash
kagi serve --db ./kagi.db --key-file ./server.key.json --bind 127.0.0.1:8787
```

On first startup, the server prints one admin token. Save it securely, then log
in from the admin machine:

```bash
kagi remote login --remote http://127.0.0.1:8787 --token kagi_admin_v1_...
```

Create a local project and request server registration:

```bash
kagi init --nested --envs
kagi remote register --remote http://127.0.0.1:8787
```

An admin approves the pending request:

```bash
kagi remote projects --remote http://127.0.0.1:8787
kagi remote approve --remote http://127.0.0.1:8787 <project_id>
```

Admin commands:

```bash
kagi remote audit --remote http://127.0.0.1:8787
kagi remote tokens --remote http://127.0.0.1:8787
kagi remote revoke-token --remote http://127.0.0.1:8787 <token_id>
```

`approve` prints a project token. Give that token to the requester once. The
token contains the remote URL, project id, and server fingerprint:

```bash
kagi remote pull <project-token>
kagi remote push
kagi remote status
```

In server mode, keep `.kagi/` local and out of Git. Project tokens are bearer
credentials and are stored outside `.kagi/`; admin tokens are stored in the OS
keychain or supplied with `KAGI_ADMIN_TOKEN`.

---

## CI and Containers

For CI, store the project key in your secret manager and mount it as a file:

```bash
KAGI_PROJECT_KEY_FILE=/run/secrets/kagi_project_key kagi run api bun test
```

`KAGI_PROJECT_KEY=<64-hex-chars>` is also supported when a file mount is not
available, but a file secret is easier to keep out of logs.

For local Docker development, prefer running the process through kagi on the
host:

```bash
kagi run api docker compose up
```

If the container itself must read env files, export them when needed and keep
`.env*` ignored by Git.

---

## Safety Model

For Git-backed projects, commit these:

```text
.kagi/kagi.json
.kagi/access.json
.kagi/secrets/**/*.enc
.env.example
```

Do not commit these:

```text
real .env / .env.* files
local project keys
local age identities / private keys
KAGI_PROJECT_KEY values
logs or screenshots containing secrets
```

The repository contains encrypted secret stores, public member recipients, and
encrypted access wrappers. It does not contain the raw project key or private
identity keys.

For server-backed projects, keep `.kagi/` local and sync encrypted state with
`kagi remote push` / `kagi remote pull` instead.

Secrets are encrypted with XChaCha20-Poly1305 and authenticated with their
scope name, so an encrypted file cannot be silently moved to another scope.

`kagi get <key>`, `kagi get --show`, and `kagi export` reveal decrypted data and
require confirmation. `kagi run` is safer for scripts, but it is not a sandbox:
the child process receives the selected secrets as environment variables.

If every active member loses their local key material and no CI secret exists,
the encrypted secrets are unrecoverable by design.

---

## Architecture

kagi is organized as a Cargo workspace with 6 crates:

| Crate | Purpose | Dependencies |
|-------|---------|-------------|
| **kagi-domain** | Core domain: entities (`Service`, `Secret`), config, errors, parsers, repository traits | None |
| **kagi-crypto** | XChaCha20-Poly1305 encryption | kagi-domain |
| **kagi-store** | Local storage (`FileStore`), key manager, env injector | kagi-domain, kagi-crypto |
| **kagi-sync** | Sync protocol types + remote client (`age`-encrypted HTTP transport) | kagi-domain |
| **kagi-server** | Axum HTTP server + SQLite remote backend | kagi-domain, kagi-sync |
| **kagi-app** | CLI application: argument parsing and command dispatch | kagi-domain, kagi-crypto, kagi-store, kagi-sync |
| **kagi-vault** | Meta-package: provides the `kagi` binary by re-exporting `kagi-app` | kagi-app |

All crates share the same version via `version.workspace = true`. The design
lets you depend on individual crates (e.g., `kagi-domain` for types, or
`kagi-crypto` for encryption) without pulling in the entire CLI or server.

---

## Development

```bash
# Run all tests (with server feature)
cargo test

# Run tests without server feature
cargo test --no-default-features

# Run integration tests only
cargo test --test integration_tests

# Run the real OS keychain smoke test
cargo test test_os_keychain_project_key_survives_local_data_loss -- --ignored

# Test a single crate
cargo test -p kagi-domain
cargo test -p kagi-store

# Try the Bun example
cd tests
kagi init --nested --envs
cd api
kagi set MESSAGE "from kagi"
bun dev

# Install locally
cargo install --path .
```

The default test suite uses isolated local storage so it can run in CI. The ignored keychain smoke test requires a real unlocked OS keychain/session and verifies that kagi can still load the project key after local data files are removed.

## Releasing

### Tag-based release (recommended)

Push a git tag and let GitHub Actions handle everything:

```bash
# Create and push tag (triggers CI: tests, builds, GitHub Release, crates.io)
VERSION=0.1.3 make tag
```

GitHub Actions workflow (`release.yml`) runs automatically on tag push:
1. Tests on all platforms
2. Builds cross-platform binaries
3. Creates GitHub Release with artifacts
4. Publishes all crates to crates.io

### Full cargo-release workflow

```bash
# Preview what a release would do
make dry-run

# Bump version, commit, tag, and push
make release

# Publish all crates to crates.io (local, not via CI)
make publish
```

`cargo-release` handles the dependency ordering automatically. See the `Makefile` for details.

---

## License

MIT