diff --git a/.vitepress/_sidebar.ts b/.vitepress/_sidebar.ts
index a76d2a7..5399c40 100644
--- a/.vitepress/_sidebar.ts
+++ b/.vitepress/_sidebar.ts
@@ -49,7 +49,7 @@ export default {
{ text: 'Configuration', link: '/settings/configuration' },
{ text: 'TLS & Certificates', link: '/settings/tls' },
{ text: 'Secrets', link: '/settings/secrets' },
- { text: 'Vault', link: '/settings/vault' },
+ { text: 'Seed', link: '/settings/seed' },
]
},
{
@@ -70,7 +70,6 @@ export default {
{ text: 'Kind', link: '/tools/kind' },
{ text: 'Python', link: '/tools/python' },
{ text: 'Rust', link: '/tools/rust' },
- { text: 'Seed', link: '/tools/seed' },
{ text: 'SSH', link: '/tools/ssh' },
{ text: 'ws-cli', link: '/tools/ws-cli' },
]
diff --git a/docs/editor/storage.md b/docs/editor/storage.md
index eb064a8..5520921 100644
--- a/docs/editor/storage.md
+++ b/docs/editor/storage.md
@@ -66,7 +66,7 @@ docker volume ls
Several workspace features store their configuration under `~/.ws` (`/home/kloud/.ws`),
including [sideloaded extensions](/editor/extensions), [startup and session scripts](/settings/autoload-scripts),
[drop-in CA certificates](/settings/tls#drop-in-directory-ws-ca-d),
-the [secrets vault](/settings/secrets), and your shell and REPL history (`~/.ws/history`).
+the [seed source](/settings/seed), and your shell and REPL history (`~/.ws/history`).
Because this directory lives inside the container, its contents are lost on restart unless
you persist it separately:
@@ -114,7 +114,7 @@ volumeMounts:
subPath: workspace
- name: data
- mountPath: /home/kloud/.ws # also persists shell/REPL history (~/.ws/history)
+ mountPath: /home/kloud/.ws
subPath: ws
- name: data
diff --git a/docs/public/icons/vault.svg b/docs/public/icons/secrets.svg
similarity index 100%
rename from docs/public/icons/vault.svg
rename to docs/public/icons/secrets.svg
diff --git a/docs/public/icons/seed.svg b/docs/public/icons/seed.svg
new file mode 100644
index 0000000..d9c6150
--- /dev/null
+++ b/docs/public/icons/seed.svg
@@ -0,0 +1,5 @@
+
diff --git a/docs/settings/secrets.md b/docs/settings/secrets.md
index de456a8..9e10249 100644
--- a/docs/settings/secrets.md
+++ b/docs/settings/secrets.md
@@ -1,8 +1,8 @@
---
description: "Manage sensitive credentials in Kloud Workspace with the ws-cli secrets command — encrypt passwords, API keys, and SSH keys instead of storing plaintext."
see:
- - name: Vault
- link: /settings/vault
+ - name: Seed
+ link: /settings/seed
- name: ws secrets Command Reference
link: /tools/ws-cli#secrets-ws-secrets
target: _self
@@ -13,7 +13,7 @@ see:
# Secrets
-{.doc-image}
+{.doc-image}
Kloud Workspace applications often require sensitive credentials: database passwords, API
keys, SSH keys, and authentication tokens.
@@ -78,10 +78,10 @@ echo "my-secret" | ws secrets encrypt - --master .master.key
cat encrypted.txt | ws secrets decrypt - --master .master.key
```
-## Vault
+## Seed
-For declarative bulk secret injection using YAML vault files, see the
-[Vault documentation](/settings/vault).
+For declarative secret projection — inline `${secrets.NAME}` values and whole-file `secret: true`
+entries in the seed manifest — see the [seed documentation](/settings/seed).
## Authentication Passwords
diff --git a/docs/settings/seed.md b/docs/settings/seed.md
new file mode 100644
index 0000000..2d7fcc6
--- /dev/null
+++ b/docs/settings/seed.md
@@ -0,0 +1,287 @@
+---
+description: Project files and secrets from a durable seed directory onto the workspace filesystem on every boot, within an ownership boundary.
+see:
+ - name: Secrets
+ link: /settings/secrets
+ - name: Autoload Scripts
+ link: /settings/autoload-scripts
+---
+
+# Seed
+
+{.doc-image}
+
+Seeding projects files and secrets from a durable source directory onto the filesystem on every
+container boot.
+Fill the source once and it re-projects each restart, with no hand-run setup scripts.
+
+One source tree carries two tiers: plain **bare files** copied verbatim, and a **`.seed.yaml`
+manifest** that layers merge, templating, and secrets over them.
+
+## At a Glance
+
+- **Source:** *(default `~/.ws/seed.d`)*. An empty or absent
+ directory is a clean no-op.
+- **Manifest:** a single `/.seed.yaml` at the source root, excluded from the bare mirror.
+- **Runs:** at boot, before the workspace's own configuration steps.
+- **Writes:** only where you own the destination, anywhere your account owns the nearest existing
+ parent directory.
+
+## Bare Files: FS-Rooted Mirror
+
+The path under the source maps directly onto the root filesystem:
+
+```text
+~/.ws/seed.d/home/kloud/.gitconfig → ~/.gitconfig
+~/.ws/seed.d/etc/workspace/x → /etc/workspace/x
+```
+
+A home file therefore lives at `seed.d/home/kloud/`, not at the source root.
+Bare files copy verbatim with mode `644` *(new directories `755`)* and never write if the
+destination already exists, unless forced.
+
+## The Manifest
+
+A single hidden `.seed.yaml` at the source root declares behaviors.
+It opens with a `version` key and two sections:
+
+```yaml
+version: v1
+
+secrets:
+ GH_TOKEN: kZ9... # inline ciphertext, or file:/run/secrets/gh_token
+
+seeds:
+ ~/.gitconfig:
+ op: merge
+
+ ~/.ssh/id_ed25519:
+ secret: true
+
+ ~/.zshenv:
+ op: append
+ content: |
+ export EDITOR=nano
+```
+
+The `seeds:` map is **keyed by destination**, `~`, `${ws_home}`, `${ws_server_root}` and
+`${ws_user}` expand.
+
+The source for each entry is implied by that key: the **rhyming mirror file**
+*(`/`)*, or an inline `content:` literal.
+There is no `file:` pointer inside `seeds:`.
+
+Each entry must carry at least one behavior, `secret`, `mode`, a non-`copy` `op`, or `template`.
+A plain copy belongs in the mirror tier, so a copy-only manifest entry is a parse error.
+
+When a destination is produced by both a bare file and a manifest entry, the manifest entry wins.
+
+## Operations
+
+`op` is one of `copy` *(default)*, `merge`, `append`, `prepend`, `block` or `lineinfile`.
+
+`merge` deep-merges structured data, with the format inferred from the destination extension
+*(`.json`, `.yaml`, `.toml`)*.
+Maps merge recursively, scalars override, and **lists replace** wholesale.
+A scalar-versus-map conflict at a key is a hard error that leaves the destination unchanged.
+
+```yaml
+seeds:
+ ~/.config/app/config.json:
+ op: merge
+ content: '{"telemetry": false}'
+```
+
+`append` and `prepend` add `content` to the end or start of the destination.
+
+::: warning
+
+`append` and `prepend` follow the same **write-if-absent** rule as every entry
+*(see [Force and Ephemerality](#force-and-ephemerality))*: an existing destination is skipped unless forced.
+
+Forced, they re-apply on every boot *(**naive, not idempotent**)*, so the content is added again each
+time and accumulates *(if a persistent volume is mounted)*.
+
+To add a single line idempotently, use [`op: lineinfile`](#managed-lines); to manage a region, use
+[`op: block`](#managed-blocks).
+
+:::
+
+### Managed Blocks
+
+`op: block` manages a marked region inside a file.
+
+It wraps `content` between two marker lines and reconciles that block on every boot, idempotently,
+even when the content changes:
+
+```yaml
+seeds:
+ ~/.zshenv:
+ op: block
+ content: "export EDITOR=nano\n"
+```
+
+The first apply appends the block and inserts the markers for you:
+
+```text
+# >>> ws-seed >>>
+export EDITOR=nano
+# <<< ws-seed <<<
+```
+
+Later applies find those markers and replace only the body between them, so the region never
+duplicates and tracks content changes.
+
+You never write the markers by hand.
+A missing file is created; an existing file keeps its content and gains the block at the end.
+
+`block` ignores `force`: it is safe to re-run, and rewrites the file only when the block's contents
+change. Malformed markers *(a begin without an end, or markers out of order)* are a hard error that
+leaves the destination unchanged.
+
+The marker text is fixed, but its comment prefix defaults to `#` and is set per entry with `comment`
+for files where `#` is not a comment:
+
+```yaml
+seeds:
+ ~/.config/app/config.js:
+ op: block
+ comment: //
+ content: "module.exports = { telemetry: false }\n"
+```
+
+```text
+// >>> ws-seed >>>
+module.exports = { telemetry: false }
+// <<< ws-seed <<<
+```
+
+`block` is plain text and does not infer a file's comment syntax; `comment` is valid only with
+`op: block`. For structured files, prefer `merge` over a marked block.
+
+### Managed Lines
+
+`op: lineinfile` manages a single line, matched by its key, idempotently:
+
+```yaml
+seeds:
+ ~/.zshenv:
+ op: lineinfile
+ content: "export EDITOR=nano\n"
+```
+
+The key is the text up to and including the first `=` *(here `export EDITOR=`)*. On each apply a
+line with that key is replaced in place; if none exists the line is appended, creating the file if
+needed. The content is exactly one line, so an interior newline is a hard error *(use `op: block`
+for a multi-line region)*.
+
+Like `block`, `lineinfile` ignores `force` and rewrites the file only when the line changes. It is
+the idempotent counterpart to `append` for `key=value` files such as `~/.zshenv`.
+
+## Templating
+
+Set `template: true` to substitute a closed variable set in the source before writing:
+
+```yaml
+seeds:
+ ~/.config/app/env:
+ template: true
+ content: "HOME=${ws_home}\nTOKEN=${secrets.GH_TOKEN}\n"
+```
+
+The available tokens are `${ws_home}`, `${ws_user}`, `${ws_server_root}` and `${secrets.NAME}`. An
+unknown `${...}` token is a hard error. There is no expression language and no escape syntax. To
+emit a literal `${...}`, leave `template` unset.
+
+## Secrets
+
+Two secret shapes share one ciphertext format, both produced by
+[`ws-cli secrets encrypt`](/settings/secrets):
+
+- **Inline values:** live in the top-level `secrets:` map *(`NAME: ` or `NAME: file:`)*
+ and are referenced only through `${secrets.NAME}` in a `template: true` entry.
+- **Whole-file secrets:** set `secret: true` on the entry. Its source
+ *(the rhyming mirror ciphertext file, or an inline `content:` ciphertext)* is decrypted and
+ written as plaintext.
+
+A secret-bearing output is forced to mode `0600`, and its cleartext never reaches logs.
+A secret that will not decrypt *(missing key, corrupt ciphertext)* is skipped with a warning and
+nothing is written, never the ciphertext, never a partial.
+
+A manifest with no secrets needs no key.
+
+::: warning
+
+Keep ciphertext files **outside** the mirror tree unless a manifest entry claims them.
+An undeclared encrypted file in the source is copied verbatim as ciphertext, exactly like any other
+bare file.
+
+:::
+
+::: info
+
+To change the master key, run [`ws-cli seed rotate`](#apply-and-inspect) — it re-encrypts every
+managed ciphertext in place under the new key.
+
+:::
+
+## Ownership Boundary
+
+Seed writes only to destinations you own, anywhere your account owns the nearest existing parent
+directory: `~/.ssh`, `~/.ws/startup.d`, dotfiles, a `/opt/mine` you created. Anything else is skipped
+with a warning.
+
+There is no `sudo` and no escalation: a location you do not own simply fails the check, which is also
+exactly where you could not write by hand.
+
+::: tip
+
+A write into `~/.ws/{startup.d,ca.d,session.d,features.d}` is allowed and emits a notice. Seeding
+your own startup script is a legitimate use. Mark it executable if it needs to run.
+
+:::
+
+## Force and Ephemerality
+
+The default is **write-if-absent**: an entry writes only when the destination is missing.
+This re-seeds ephemeral paths every boot for free and preserves a hand-edited persistent file.
+To overwrite an existing destination, set `force: true` on the entry or pass `--force` to re-apply
+everything.
+For `merge`, `force` gates the merge too, and without it an existing destination is left alone.
+
+## Apply and Inspect
+
+The boot hook runs `ws-cli seed apply` with no arguments.
+Run it by hand to re-project, or scope it to specific destinations:
+
+```sh
+# Project everything
+ws-cli seed apply
+
+# Re-apply, overwriting existing destinations
+ws-cli seed apply --force
+
+# Project a single destination
+ws-cli seed apply ~/.gitconfig
+
+# List the resolved plan
+ws-cli seed ls
+```
+
+A named destination matches its entry regardless of `~`, `$HOME` or absolute form.
+
+Rotate the master key across every managed ciphertext — the `secrets:` map, its `file:` targets, and
+every `secret: true` source — in one pass:
+
+```sh
+# Re-encrypt everything from the current key to a new one
+ws-cli seed rotate --master --new-master
+```
+
+::: warning
+
+`seed rotate` rewrites ciphertext **in place** — there is no dry-run and no backup. It fails closed:
+every secret is decrypted under the current key *before* anything is written, so a wrong key changes
+nothing. If a run is interrupted, re-run it with the new key to finish.
+
+:::
diff --git a/docs/settings/tls.md b/docs/settings/tls.md
index 69d11b6..5c3b510 100644
--- a/docs/settings/tls.md
+++ b/docs/settings/tls.md
@@ -126,7 +126,7 @@ docker run \
This is the recommended path for ad-hoc certificate injection on a
running workspace — the same persisted volume already used for
[autoload scripts](/settings/autoload-scripts), extensions, and the
-[secrets vault](/settings/vault).
+[seed source](/settings/seed).
### Install Certificate from HTTPS Endpoint
diff --git a/docs/settings/vault.md b/docs/settings/vault.md
deleted file mode 100644
index ebfdd93..0000000
--- a/docs/settings/vault.md
+++ /dev/null
@@ -1,127 +0,0 @@
----
-description: "YAML vaults in Kloud Workspace enable declarative, bulk secret injection, building on the ws-cli secrets encryption primitives."
-see:
- - name: Secrets
- link: /settings/secrets
- - name: ws secrets Command Reference
- link: /tools/ws-cli#secrets-ws-secrets
- target: _self
----
-
-# Vault
-
-{.doc-image}
-
-YAML vaults allow bulk secret injection into the workspace. Vaults build on the
-[encryption primitives](/settings/secrets) provided by `ws-cli secrets` and add structured,
-declarative secret management.
-
-## Vault Format
-
-A vault is a YAML file listing one or more secrets with their encrypted values,
-destinations, and types:
-
-```yaml
-secrets:
- database-password:
- encrypted: Xy1z2A3...
- destination: /workspace/.secrets/db-password
- type: generic
- mode: 0o600
-
- api-key:
- encrypted: Ab1c2D3...
- destination: API_KEY
- type: env
-
- ssh-private-key:
- encrypted: Kl1m2N3...
- destination: ~/.ssh/id_rsa
- type: ssh
-```
-
-:::warning
-
-Vault secrets can only be written to user-writable paths such as `$HOME`, `/workspace`, and `/tmp`.
-
-System paths like `/etc/` and `/usr/` are protected by Linux file permissions since the workspace
-runs as the `kloud` *(non-root)* user.
-
-:::
-
-## File References
-
-For long encrypted values, store them in separate files using the `file:` prefix:
-
-```yaml
-secrets:
- ssh-private-key:
- encrypted: file:./secrets/ssh-key.enc
- destination: ~/.ssh/id_rsa
- type: ssh
-```
-
-File paths are relative to the current working directory.
-
-## Secret Types
-
-Each secret has a `type` field that determines how it's written and its default permissions:
-
-| Type | Destination | Default Mode | Description |
-| ------------------ | ------------- | ------------ | ---------------------------------- |
-| `generic` | File path | `0o600` | General-purpose secrets |
-| `ssh` | File path | `0o600` | SSH private keys |
-| `env` | Variable name | `0o644` | Written to `~/.zshenv` |
-| `kubeconfig` | File path | `0o600` | Kubernetes config files |
-| `dockerconfigjson` | File path | `0o600` | Docker authentication config files |
-
-- **`--destination`:** For file types *(generic, ssh, kubeconfig, dockerconfigjson)*, this
- is the file path where the secret will be written. Relative paths are resolved
- from `/workspace`.
- For `env` types, this is the environment variable name.
-- **`--mode` *(optional)*:** File permissions in octal *(e.g., `0o600`)* or decimal *(e.g., `384`)*.
- If omitted, the default mode for the type is used.
-- **`--type` *(optional)***.
-
-## Processing a Vault
-
-```sh
-# All secrets
-ws secrets vault --input vault.yaml --master .master.key
-
-# Specific keys
-ws secrets vault --input vault.yaml --key database-password --master .master.key
-
-# Inspect values
-ws secrets vault --input vault.yaml --stdout --master .master.key
-```
-
-## Autoloading
-
-At startup the workspace automatically processes `~/.ws/vault/secrets.yaml` when it exists.
-Place your vault manifest and any referenced encrypted files together in that directory:
-
-```text
-~/.ws/vault/
-├── secrets.yaml # vault manifest
-├── db-password.enc # encrypted file referenced via file:
-└── ssh-key.enc
-```
-
-`~/.ws/vault/secrets.yaml` is the sole supported autoload location. To use
-a vault stored elsewhere on the host, bind-mount it into the convention
-path:
-
-```sh
-docker run \
- -v /host/path/secrets.yaml:/home/kloud/.ws/vault/secrets.yaml \
- ghcr.io/kloudkit/workspace:v0.4.0
-```
-
-## Vault Flags
-
-- **`--input `:** Vault file path *(defaults to `~/.ws/vault/secrets.yaml`)*.
-- **`--key `:** Process specific secret *(repeatable)*.
-- **`--stdout`:** Inspect without writing.
-
-See [ws secrets command reference](/tools/ws-cli#secrets-ws-secrets) for complete syntax.
diff --git a/docs/tools/seed.md b/docs/tools/seed.md
deleted file mode 100644
index 98a8daf..0000000
--- a/docs/tools/seed.md
+++ /dev/null
@@ -1,155 +0,0 @@
----
-description: Project files from a durable seed directory onto the workspace filesystem on every boot, within hard security boundaries.
-see:
- - name: Git
- link: /tools/git
- - name: Ansible
- link: /tools/ansible
- - name: Autoload Scripts
- link: /settings/autoload-scripts
----
-
-# Seed
-
-Seeding projects files from a durable source directory onto the filesystem on every container boot.
-Fill the source once and it re-projects each restart, with no hand-run setup scripts.
-
-There are two tiers in a single source tree:
-
-- **Bare files**: an FS-rooted mirror of the target filesystem.
-- **A `.seed.yaml` manifest**: an Ansible tasks-list run through a hardened wrapper play.
-
-## At a Glance
-
-- **Source:** *(default `~/.ws/seed.d`)*.
- An empty or absent directory is a clean no-op.
-- **Runs:** on every boot, before the workspace's own configuration steps.
-- **Writes:** only under `$HOME` and `${WS_SERVER_ROOT}`, minus a fixed deny-set.
- System directories require an explicit opt-in.
-
-## Bare Files: FS-Rooted Mirror
-
-Plain files mirror the target filesystem tree.
-The path under the source maps directly onto the root filesystem:
-
-```text
-~/.ws/seed.d/home/kloud/.gitconfig → ~/.gitconfig
-~/.ws/seed.d/etc/workspace/x → /etc/workspace/x
-```
-
-A home file therefore lives at `seed.d/home/kloud/`, not at the source root.
-Each file is written with a fixed mode *(644 for files, 755 for new directories)* and owned by `kloud`.
-Bare files reconcile every boot, overwriting the destination whether or not it changed.
-
-## Task Tier: `.seed.yaml`
-
-A single hidden `.seed.yaml` at the source root is an Ansible **tasks-list** *(not a full play)*.
-It is excluded from the bare mirror and never copied verbatim.
-The workspace generates the play it controls *(`localhost`, local connection, no fact-gathering, `become: false`)*
-and runs it with the plugin path emptied from a clean temporary directory.
-
-Supported modules: `copy`, `template`, `file`, `blockinfile`, `lineinfile` and `set_fact`
-*(for `combine` ergonomics)*.
-
-Inline `content:` may template over a closed set of variables *(`ws_home`, `ws_user` and `ws_server_root`)*,
-and the `combine` filter performs YAML/JSON deep-merge.
-
-```yaml
-- name: Write a merged config
- copy:
- dest: "{{ ws_home }}/.config/app/config.json"
- content: "{{ {'theme': 'dark'} | combine({'telemetry': false}) | to_nice_json }}"
-
-- name: Append a shell line once
- lineinfile:
- path: "{{ ws_home }}/.bashrc"
- line: export EDITOR=nano
-```
-
-### Propagation
-
-Task entries inherit Ansible's native `force: true`, reconciling every boot.
-Set `force: false` per entry for "seed-once, then let the destination drift".
-There is no global mode switch.
-
-::: info
-
-`force: false` only applies to `copy` and `template`. `file`, `blockinfile` and `lineinfile` have
-no `force` and reconcile every boot: a `lineinfile` with a non-matching `regexp` appends a duplicate
-line across boots.
-
-:::
-
-## Security Boundaries
-
-Three boundaries constrain the seed. They hold in every mode and are never governed by `force`.
-
-### Deny-Set
-
-Any path a later startup script or shell-init autoloads, executes, or feeds to a root-capable process
-is rejected: the seed can never plant code or trust that a less-hardened consumer later runs.
-A rejected entry is skipped with a warning; the boot continues.
-
-Rejected destinations include `~/.ws/{startup.d,session.d,ca.d,features.d,extensions}`,
-`~/.ws/{vault,state,history}`, `~/.ssh`, `~/.kube`, `~/.zshenv`, any `.git/` directory, and system
-paths such as `/etc/sudoers.d`, `/etc/ssh`, `/etc/ansible`, `/etc/profile.d`, `/usr`, `/bin` and
-`/sbin`.
-
-### System-Tier Gate
-
-The `.seed.yaml` task tier never writes system paths.
-System seeding is bare-file plain-copy only, gated by two opt-ins:
-
-- set to `true`
-- password-less `sudo` available *(`WS_AUTH_DISABLE_SUDO=false`)*.
-
-The system deny-set is rejected even when `allow_system` is on.
-
-### Hardened Mode
-
-When `WS_AUTH_DISABLE_SUDO=true`, the seed runs the bare-file copy tier into user-space only: no
-Ansible interpreter is invoked at all, and `seed.allow_system` is ignored.
-
-A system-path bare file is a clean skip, never a half-write.
-
-## Edit the Source, Not the Projection
-
-The seed is **reconcile**: edit `~/.ws/seed.d`, not the live projection.
-A change made directly to a seeded destination on a persistent volume reverts on the next boot.
-
-Reconcile is also **additive**: removing a file from the source does **not** delete its earlier
-projection; the projection survives the next boot.
-
-::: warning
-
-The seed is a **base layer, not the source of truth.**
-
-Because it runs before the workspace's own configuration steps, the files those steps manage
-*(the editor `settings.json`, shell configuration and server configuration)* are overridden by the
-workspace on every boot. Seeding wins for everything else; it loses for the files the workspace
-configures.
-
-:::
-
-::: warning
-
-`combine` merges inline and seeded fragments only.
-It **cannot** read an existing on-disk file to patch an override into it: reading for merge is not
-available to the task tier.
-
-:::
-
-::: danger
-
-Seeding into `${WS_SERVER_ROOT}` trips the clone guard and **suppresses** `WS_GIT_CLONE_REPO`.
-If the server root is a persistent volume, reconcile reverts live edits on every boot.
-
-A whole repository cannot be planted: any `.git/` path is denied.
-
-:::
-
-## Next Steps
-
-- [Git](/tools/git): automated repository cloning into `${WS_SERVER_ROOT}`.
-- [Ansible](/tools/ansible): the engine behind the `.seed.yaml` task tier.
-- [Autoload Scripts](/settings/autoload-scripts): the `~/.ws/*.d` drop-in convention.
diff --git a/docs/tools/ws-cli.md b/docs/tools/ws-cli.md
index 9a7a8f6..4d1d531 100644
--- a/docs/tools/ws-cli.md
+++ b/docs/tools/ws-cli.md
@@ -109,8 +109,8 @@ ws logs --level=error --tail=100 --follow
Manage encryption, decryption, and master key generation for secure secrets handling.
-For comprehensive documentation including vault management, secret types, and security
-best practices, see the [dedicated secrets documentation](/settings/secrets).
+For comprehensive documentation including secret types and security best practices, see the
+[dedicated secrets documentation](/settings/secrets).
#### Quick Reference
@@ -119,7 +119,15 @@ best practices, see the [dedicated secrets documentation](/settings/secrets).
- **`login`:** Generate a login password hash for authentication.
- **`encrypt `:** Encrypt a plaintext value.
- **`decrypt `:** Decrypt an encrypted value.
-- **`vault`:** Process a vault file and write secrets to destinations.
+
+### Seed (`ws seed`)
+
+Project files and secrets from a durable source directory onto the filesystem. See the
+[seed documentation](/settings/seed) for the manifest schema and ownership boundary.
+
+- **`apply [dest...]`:** Project the seed plan, optionally scoped to named destinations *(`--force`
+ overwrites existing destinations)*.
+- **`ls`:** List the resolved seed plan.
### Serve (`ws serve`)