Skip to main content
Version: 2.10

Manage environment variables and secrets

Shine stores variables used by preset templates and can seal sensitive project-environment values with GPG or age. Sealed secrets can be injected into local child processes or requested for a remote command through the SSH Secret Broker while decryption remains local. Never put real secrets in a public repository or documentation example.

Secret operations live under shine env secret. Workspace-based shine env run and env run --with inject values on demand. For remote commands, first choose between direct forwarding and brokered on-demand decryption as described below.

Inspect and set values​

Choose commands for the operation you need; do not run this as a set/delete sequence:

OperationCommand
List variables and their sourcesshine env list
Read the effective valueshine env get HTTP_PROXY_PORT
Inspect precedence and sourcesshine env explain HTTP_PROXY_PORT
Save a valueshine env set HTTP_PROXY_PORT 6152
Delete the selected stored valueshine env delete HTTP_PROXY_PORT

PROXY_NO_PROXY controls the NO_PROXY and no_proxy values set by setproxy and defaults to localhost,127.0.0.1,::1. After changing it or another proxy variable, shine update marks the installed proxy shell preset as updatable; run shine upgrade to apply the value.

The built-in image commands use IMAGE_QUALITY=80, IMAGE_MAX_WIDTH=1920, and IMAGE_MAX_HEIGHT=1080 by default. Override them for one run with --quality, --width, or --height, or keep different machine-local defaults with shine env set.

shine env list hides sensitive values by default; use --reveal only in a trusted terminal. It groups effective values by builtin, overlay, global config, global env file, project config, and project env file, with actual file paths and the current project root. env explain KEY shows that storage key's definitions from low to high, its winning layer and description source, hiding all values by default. --reveal never decrypts. Explicit injection still prefers KEY_SECRET over KEY; explain and write output point out an existing encrypted key. Defaults are runtime fallbacks: queries create no configuration. Setting a value equal to a builtin still records an explicit entry.

If Shine reports invalid TOML, correct the indicated file or field before retrying the edit.

Global ~/.shine/config.toml and project shine.config.toml support either a string or a value with a description:

[env]
HTTP_PROXY_PORT = "6152"
MY_API_TOKEN = { value = "<token>", description = "Token for the internal API" }

value behaves exactly like the short string form; description appears in shine env list. Running shine env set MY_API_TOKEN <new-value> on a detailed entry updates value and preserves the description.

Write targets depend on options and cwd, independently of the effective source:

OptionsTarget
NoneActive project shine.config.toml [env], otherwise global config
--globalGlobal config.toml [env]
--projectNearest project [env]; fails without an existing project
--env-fileSelected or default scope's shine.env.toml
--overlayActive manual overlay's shine.env.toml

Scope options are mutually exclusive. --overlay --env-file selects the same overlay file. Scripts should specify both scope and file options to avoid cwd dependence. With no target options, a key defined in a higher layer is protected, even if values match. Explicit targets allow maintenance of shadowed entries and report the saved file, current effective source, and whether the value changed.

Choose the operation and storage location you need:

OperationCommand
Write global configurationshine env set HTTP_PROXY_PORT 7890 --global
Write project configurationshine env set HTTP_PROXY_PORT 8080 --project
Write the project env fileshine env set HTTP_PROXY_PORT 8081 --project --env-file
Write the manual overlayshine env set HTTP_PROXY_PORT 6152 --overlay
Delete from the global env fileshine env delete HTTP_PROXY_PORT --global --env-file
Save an encrypted copy of the effective MY_TOKEN as MY_TOKEN_SECRET in the project env fileshine env secret encrypt --from MY_TOKEN --set MY_TOKEN_SECRET --project --env-file

The encryption command above preserves the plaintext MY_TOKEN in its original layer.

Deletion removes only an existing entry in the selected file; an absent local entry is an error. It restores inheritance, never disables a variable, removes other layers, or also deletes KEY_SECRET. Empty strings remain valid values. Managed Git overlay mirrors reject writes: edit upstream, use a manual maintenance directory, or choose local overrides. These three write entries reject the retired --force without writing and suggest the correct replacement options.

Avoid editing configuration while an env write is in progress. If Shine reports a concurrent change, inspect the file and retry. A message saying the file was saved but its effective state could not be verified means the write happened; query the value again before making another edit.

Writes preserve comments and unrelated entries, and reject symbolic links and special files. Unix files use owner-only permissions, but unencrypted values remain plaintext.

Global, overlay, and project shine.env.toml files omit the [env] header and support both forms:

HTTP_PROXY_PORT = "7890"
PROXY_HOST = { value = "127.0.0.1", description = "Local proxy host" }

Strings and detailed entries with only value inherit lower-layer descriptions. An explicit description replaces the description; an empty string clears it and disables preset catalog fallback. Invalid entries—numbers, arrays, or a table without value—fail explicitly.

After changing a value used by template rendering, run:

shine upgrade

Manual upgrade and rollback for env layering​

The unified order is builtin defaults < overlay baseline < global [env] < global env file < project [env] < project env file. Previously overlays outranked global env files and every env file outranked both [env] layers. Upgrading can change values; there is no old-order mode or automatic migration.

  1. Before upgrading, record the actual runtime root, important projects, and key sources. Make private backups of files you plan to edit; keep secrets out of repositories and public logs.
  2. Replace old --force scripts: use --global --env-file for global env files, --project --env-file for project env files, and --overlay for manual overlays. Maintain managed mirror values upstream or select local overrides.
  3. After upgrading, run env list and env explain KEY outside projects and in each important project. Projects can select different overlays; one project's result does not describe another.
  4. All historical entries remain explicit, including auto-written entries equal to old defaults. Delete selected entries with explicit targets only after review, then check inheritance again; equality with a default alone is no reason to delete. Only keys without explicit definitions follow new builtin defaults. To inherit overlay values, remove unneeded local or project entries.
  5. Check KEY_SECRET: changing or deleting plaintext does not cancel encrypted-first injection. Run shine update, review the shine upgrade Plan, and apply installed-artifact changes through that existing workflow. Editing config or upgrading the binary never rerenders, runs, or approves presets automatically.

Downgrading the binary restores old precedence, but cannot restore deleted entries or updated installed artifacts. Review and restore private backups as needed without overwriting unrelated edits; artifact changes still require lifecycle review. There is no env-specific migration or recovery command. Old binaries may also resume expanding missing builtin defaults into global config; this does not recover deleted custom values.

Encrypt values with GPG​

Install the selected encryption backend (gpg or age) and any identity plugins it requires.

First make sure local gpg can use the recipient public key. For a private key on YubiKey, see the Chinese guide 在 macOS 和 Windows 使用 YubiKey OpenPGP. Set one or more default recipients in ~/.shine/config.toml:

gpg_recipients = ["user@example.com", "team-backup@example.com"]

Encrypt an existing plaintext value into another key:

shine env secret encrypt --from MY_TOKEN --set MY_TOKEN_SECRET
shine env secret decrypt MY_TOKEN_SECRET

Encryption needs only recipient public keys. Decryption requires the YubiKey containing the private key and may prompt for a PIN or touch.

Legacy gpg_key_id is deprecated. Preview and apply migration with shine state migrate --dry-run and shine state migrate; a workspace using the old [env.encryption].recipient is prompted to migrate when env run or env secret seal needs it.

Export a value into the current shell:

Choose whether to keep or rename the variable:

export_code="$(shine env secret export MY_TOKEN)" && eval "$export_code"
export_code="$(shine env secret export MY_TOKEN --as API_TOKEN)" && eval "$export_code"

In PowerShell, evaluate the generated code only after a successful export:

$exportCode = shine env secret export MY_TOKEN --as API_TOKEN
if ($LASTEXITCODE -eq 0) { Invoke-Expression $exportCode }

PowerShell exports preserve Unicode and multiline values, including their original line endings. After installing the utils shell preset, shine-env-export MY_TOKEN --as API_TOKEN is equivalent. Values containing NUL cannot be represented in environment variables. env secret export rejects them; env get and env secret decrypt can still return the original value.

Use age identities​

The age backend is suitable for committing team ciphertext encrypted to multiple member recipients. Existing GPG ciphertext needs no migration: legacy untagged ciphertext continues to use GPG, while new age ciphertext has an age: tag.

First ensure age 1.3 or newer is installed and available on PATH. Create a normal software identity and record its recipient:

shine env secret identity init
shine env secret identity list

A normal identity defaults to ~/.shine/age/identity.txt. Use --output to choose another path, then add it to your age_identity or age_identities configuration as shown by the command. Paths used in TOML must be valid UTF-8. identity list shows configured identity paths and recipients.

Replacing an existing identity requires --force. Keep a secure backup if you still need it to decrypt older data. Unix identity files are accessible only to their owner.

Use Touch ID on macOS​

On macOS, choose a Secure Enclave identity instead when decryption should require local user authorization. It also requires age-plugin-se; Homebrew can install both dependencies:

brew install age age-plugin-se

Run the Touch ID form instead of the normal identity initialization above, then record its recipient:

shine env secret identity init --touch-id
shine env secret identity list

--touch-id is macOS-only. Decryption requires the local Secure Enclave and prompts for Touch ID or the system passcode; copying the identity file to another machine is normally insufficient to decrypt. Cancel unexpected authorization prompts and inspect the command that caused them. For the security boundary when AI agents can run local commands, see Protect environment secrets when using AI agents.

New Touch ID identities use an age1tag... recipient. Age 1.3 can encrypt to that public recipient on macOS, Linux, or Windows without installing age-plugin-se; only decryption needs the plugin and the original Mac. Tagged recipients trade that portability for discoverability: someone who already knows the recipient can test whether a ciphertext targets it.

Configure machine-wide defaults in ~/.shine/config.toml:

secret_backend = "age"
age_recipients = ["age1tag1qexample...", "age1qteammate..."]
age_identity = "~/.shine/age/identity.txt"
age_identities = ["C:/Users/<user>/AppData/Local/age-plugin-phone/identity-....txt"]

The legacy age_identity path and the additional age_identities list are merged in order and deduplicated. This lets a normal or Secure Enclave identity coexist with hardware-plugin stubs without copying either file.

A project-team recipient list belongs in [env.encryption] in the project's shine.workspace.toml. It can be committed and overrides global defaults without affecting other projects. Never commit the private age_identity.

Select a backend and recipients for one command:

shine env secret encrypt --backend age -r age1tag1qexample... -r age1qteammate... --from MY_TOKEN
shine env secret seal --backend age -r age1tag1qexample... -r age1qteammate...

-r/--recipient is repeatable for both GPG and age.

Older Secure Enclave identities may have an age1se... recipient, which requires age-plugin-se even on a machine that only encrypts. From the project containing the workspace, preview and apply the public-recipient conversion:

OperationCommand
Preview legacy-state migrationshine state migrate --dry-run
Apply after reviewing the previewshine state migrate

Migration converts configured age1se... recipients to equivalent age1tag... recipients without decrypting secrets. Reseal afterward to use the new recipients. Existing ciphertext remains unchanged and can still be decrypted.

Add recipients to existing workspace secrets​

Changing age_recipients alone does not update existing ciphertext. Keep the existing recipients and append the new members' public recipients in shine.workspace.toml:

[env.encryption]
backend = "age"
age_recipients = ["age1existing...", "age1newmember...", "age1backup..."]

Replace every placeholder with an actual recipient, then run this on a machine whose configured identity can still decrypt the old payloads:

shine env secret seal

seal decrypts the old payloads and encrypts them again for the complete current recipient list. Even when every [secret] entry is true, its value is retained and re-encrypted; leave these entries as true, without exporting plaintext or entering the values again. Only the new members' public recipients are needed, not their private identities. Decrypting old payloads may still require Touch ID, phone authorization, or another identity-specific prompt.

Without a file argument, seal processes existing sources referenced by the workspace across its configured modes, including default_mode. Use shine env secret seal <FILE> to process only one source. If sealing stops with an error, some files may already be updated; resolve the reported error and rerun it before sharing the result.

Commit the updated workspace configuration together with the resealed shared source files. Each listed recipient can independently decrypt the new ciphertext; have new members verify access on their own machines. New recipients cannot decrypt old ciphertext in Git history. Likewise, removing a recipient and resealing does not revoke its access to historical ciphertext or rotate the real upstream credentials; rotate exposed credentials at their source when necessary.

Experiment with phone authorization on Windows and macOS​

age-plugin-phone now offers a limited technical Beta for synthetic or disposable data, not real or production secrets. Follow its Beta release notes and Windows Beta quick start for installation, matching desktop/phone artifacts, pairing, and recovery checks. Keep an independently verified recovery recipient.

Windows requires Windows 11 x64, TPM 2.0, Microsoft Platform Crypto Provider, and a capability-qualified Android StrongBox phone. Windows and macOS support Cargo source installation; follow the plugin's build prerequisites. The optional Windows ZIP is test-signed: do not import its private signing root into a system trust store. macOS has no installer package.

Shine also opens the experimental macOS pairing flow. Follow the plugin's macOS source quick start with a matching Android StrongBox app. It requires real Secure Enclave support in a logged-in user session; Intel/T2 and other hardware or OS versions are not generally verified. A compilation deployment floor is not a tested minimum supported macOS version. iPhone evidence is limited to existing development devices; the Beta does not provide an externally installable iOS app. The plugin performs the hardware checks.

The Beta's validated transport scope is Android Developer USB and foreground Wi-Fi on the recorded devices. QR with tagged recipients, BLE, and background wake are outside that scope. On macOS, auto can select QR when no Wi-Fi listener responds: keep the phone's Wi-Fi listener ready or explicitly use Android ADB for this workflow. Do not restore older plugin replay state to recover a failed operation; follow the plugin's recovery instructions.

For example, on a Mac with an authorized Android ADB device:

shine env secret identity init --phone --label "Work Mac" --transport adb --adb-serial SERIAL

After installing the matching desktop plugin and Android application, start its transactional pairing through Shine:

shine env secret identity init --phone --label "NUC WiFi Pair" --transport auto

Before running the command, open the phone's explicit Pair · Wi-Fi action if you want to pair over the local network. On Windows and macOS, auto performs one bounded Wi-Fi discovery first. Exactly one matching foreground phone listener selects Wi-Fi; if no listener responds, setup selects Developer USB/ADB on Windows or QR on macOS before creating the pairing offer. QR requires a supported camera and the plugin's scan flow. Ambiguous discovery or a local discovery error fails closed, and an attempt never switches transport after protocol work begins. auto is the default, so omitting --transport auto keeps the same policy.

Developer USB uses the opposite order. Start the desktop command first; after the plugin has selected ADB and is waiting for the phone connection, choose Pair · USB on the phone. With --transport adb, ADB is selected directly after preflight. The phone makes one immediate connection attempt, so choosing Pair · USB before the desktop has armed its reverse rule reports usb_transport_failed.

The pairing label defaults to the computer name on Windows and macOS, or Shine desktop if no valid name is available. Explicit labels must be nonblank and at most 64 UTF-8 bytes. Override it, pin Developer USB or QR, or select one of multiple ADB devices explicitly when needed:

Choose one pairing method for this device:

shine env secret identity init --phone --label "Work laptop"
shine env secret identity init --phone --transport adb
shine env secret identity init --phone --transport qr
shine env secret identity init --phone --adb-serial SERIAL

After successful fingerprint confirmation, Shine adds the identity path to the current user's global age_identities. If the active project explicitly overrides age_identity or age_identities, the command stops before pairing instead of creating an identity that the project would ignore. A manual configuration has this shape (use the actual absolute identity path returned by the plugin on your platform):

age_identities = ["/absolute/path/returned/by/plugin/identity.txt"]

If decryption fails only when multiple phone identities are configured, enable diagnostics with AGE_PLUGIN_PHONE_MESSAGES=1 and check whether the plugin needs upgrading. As a temporary workaround, configure only the matching identity path; do not delete pairing or recovery state.

The shortcut does not change secret_backend and does not add recipients. Interrupted plugin setup must be handled with age-plugin-phone setup --resume or age-plugin-phone setup --cleanup as described by the plugin; do not start a second pairing as recovery.

Put the printed age1tag... recipient and an independently verified recovery recipient in the project's commit-ready shine.workspace.toml:

[env.encryption]
backend = "age"
age_recipients = ["age1tag...", "age1..."]

Shine requests --recipient-type tag by default. This requires age 1.3+ before pairing and a desktop plugin and phone app that both support tagged recipients, available starting with 0.1.0-beta.1. The older alpha.5 release does not include this capability. Shine does not retry setup or fall back to another recipient type. age1tag... encryption needs only age 1.3+ on the encrypting computer, with no phone plugin or phone prompt. Pairing and phone decryption still need the plugin and matching phone app.

To retain the plugin's phone recipient format, explicitly select:

shine env secret identity init --phone --recipient-type phone

age1phone... encryption still requires age-plugin-phone on every encrypting computer. The plugin's own default remains phone; Shine explicitly requests its chosen type. Unlike phone v2's private recipient selection, a tagged recipient lets someone who knows the recipient test whether a ciphertext targets it.

For an existing pairing, first upgrade and verify the desktop plugin and phone app's tag support, then export its public tag recipient without pairing again:

age-plugin-phone recipients -i <IDENTITY_STUB> --recipient-type tag

Replace the corresponding recipient in global/project age_recipients or workspace [env.encryption].age_recipients, retaining the independently verified recovery recipient, then reseal. shine state migrate does not convert phone recipients. Export does not rewrite the stub or existing ciphertext; identity list continues to show the recipient recorded in the stub. Resealing an existing payload first decrypts it and may require phone authorization; see Add recipients to existing workspace secrets. Changing recipients does not revoke access to historical ciphertext. For later Wi-Fi-first decrypts with an auto pairing, enable Wi-Fi auto-listen and keep the phone app in the foreground. The plugin discovers the matching listener before creating the unwrap request and otherwise selects Developer USB/ADB on Windows or QR on macOS; it does not race routes or retry in flight. Decrypting a phone-backed secret, including through shine env run, invokes the standard age plugin and must require a fresh strong biometric authorization for each file-key unwrap. Developer USB and Wi-Fi plugin guidance is quiet by default; set AGE_PLUGIN_PHONE_MESSAGES=1 to opt into it. Explicit QR requests remain visible because the phone must scan them. Never make the experimental phone recipient the only recipient for retained data; the recovery path must not depend on the same phone StrongBox keys, desktop TPM/Secure Enclave keys, or plugin state.

If AI agents participate in development, read Protect environment secrets when using AI agents first to understand the boundaries around identity files, Touch ID, phone authorization prompts, and command execution.

Provide values to one command​

Use repeatable --with without changing the current terminal or creating workspace files:

Choose one injection method:

OperationCommand
Inject under the same nameshine env run --with MY_TOKEN -- bun run build
Rename for the child processshine env run --with MY_TOKEN=API_TOKEN -- bun run build
Inject multiple variablesshine env run --with TOKEN_A --with TOKEN_B=OTHER_TOKEN -- bun run build
Skip workspace loadingshine env run --no-workspace --with MY_TOKEN -- bun run build

Each key prefers <KEY>_SECRET and falls back to plaintext <KEY>. The name after = is the child process variable. Explicit --with values override the current process and workspace.

--no-workspace skips shine.workspace.toml discovery completely and merges only the current process and explicit --with values. It cannot be combined with --workspace or --mode. Managed Bun entries use this mode when they need fixed Shine configuration independent of the working directory.

Choose one-time injection or a transparent wrapper​

Use one-time injection for an occasional sensitive operation. For example, Cargo accepts CARGO_REGISTRY_TOKEN for crates.io when its cargo:token credential provider is active, so a yank can receive the token without leaving it in the shell or keeping a command wrapper enabled:

shine env run --no-workspace \
--with CARGO_REGISTRY_TOKEN \
-- cargo yank my-crate@1.2.3

--with CARGO_REGISTRY_TOKEN prefers encrypted CARGO_REGISTRY_TOKEN_SECRET and injects the plaintext only into Cargo for this run. Cargo and any descendants it launches can still read the value. For ordinary persistent Cargo authentication, Cargo recommends an operating-system credential provider; use Shine injection when you intentionally keep the token encrypted in Shine. See Cargo registry authentication and cargo yank.

Install a transparent wrapper for fixed credential variables​

Use a transparent wrapper when a CLI repeatedly needs the same fixed credential variable. Some CLIs, such as GitHub CLI, read a variable like GH_TOKEN instead of accepting it as an argument:

shine env proxy install gh --with GH_TOKEN
gh pr list

Shine creates a same-name shim in ~/.shine/bin/ and records the real command found in PATH. The shim resolves GH_TOKEN_SECRET only for its child and falls back to plaintext GH_TOKEN. It never exports the value back to the parent. --with is repeatable and accepts KEY=ALIAS.

The proxy forwards child stdout/stderr and preserves its numeric exit code. On Unix, termination by a signal returns 128 + signal (for example, SIGTERM returns 143), including when injection is disabled or no rule applies.

The shim retains the absolute runtime directory selected during installation, including a custom --config-dir. Later calls use that directory even when the caller's SHINE_CONFIG_DIR differs. Project rules still follow the caller's working directory. Re-run env proxy install to update an older shim or move it to another runtime directory. Proxy installation requires UTF-8 configuration and executable paths.

An installed proxy is command-wide, not subcommand-specific. If you deliberately proxy Cargo, disable injection until it is needed:

shine env proxy install cargo --with CARGO_REGISTRY_TOKEN
shine env proxy disable cargo

# Later, for the credentialed operation:
shine env proxy enable cargo
cargo yank my-crate@1.2.3
shine env proxy disable cargo

While enabled, every Cargo subcommand and any descendant process may inherit the token. Disabling the rule retains the shim and forwards directly to the real Cargo without decrypting or injecting values. For an occasional yank, prefer the one-time env run form above.

Proxy only a command you trust, using a bare name containing ASCII letters, numbers, -, _, or .. Put ~/.shine/bin/ early in PATH. The target must be an executable outside Shine's command directory and cannot be another Shine wrapper or Shine itself.

On Windows, use gh, not gh.exe: .exe, .com, .bat, .cmd, and .ps1 suffixes are rejected. Previously installed suffixed names can still be removed with shine env proxy uninstall <COMMAND>.

Shine preserves conflicting files during installation or removal. Inspect reported conflicts before retrying. If an operation fails or is interrupted, check the wrapper and configuration before using it again; do not assume recovery completed.

Rules default to global ~/.shine/config.toml. Inside a project with shine.config.toml, add --project to scope the rule; a project rule for the same command overrides the global one. Outside that project, the shim uses the global rule if present; otherwise it forwards to the real command without injecting values:

shine env proxy install gh --with GH_TOKEN --project
shine env proxy list

Disable injection temporarily while retaining the shim; the disabled wrapper directly forwards to the real program:

Choose the state and scope you need:

OperationCommand
Disable the proxyshine env proxy disable gh
Enable the proxyshine env proxy enable gh
Disable the project proxyshine env proxy disable gh --project

Without --project, enable/disable changes the global rule even inside a project. A project rule can still override it. If Shine reports that the rule was saved but its effective state could not be verified, inspect the configuration and run shine env proxy list again.

Remove the managed shim and user-level rule when no longer needed:

shine env proxy uninstall gh

If the real executable moves or is replaced, rerun the install command to record its new path.

Provide variables and secrets to remote commands​

Choose according to how widely plaintext may be visible remotely:

GoalLocal commandPlaintext visibility
Forward a normal valueshine ssh --with API_URL devRemote login shell or specified command
Decrypt and forward a secret directlyshine ssh --with-secret API_TOKEN devRemote login shell or specified command
Let an authorized remote child request local decryptionshine ssh --secret-broker ... devOnly the approved remote child process

Direct secret forwarding suits temporary work on a trusted host, but the remote login shell and same-account processes may read the plaintext. Use the SSH Secret Broker when the decryption key must stay local and only a selected remote child should receive the value. The remote administrator or same-account processes can still inspect that child.

For setup commands, local policies, enrollment, and the complete security boundary, see SSH sessions, secret brokering, and file transfer.

Initialize a workspace from dotenv​

At a project root with Vite-style .env files, generate a Shine workspace and TOML sources:

OperationCommand
Preview importing dotenv filesshine env workspace init --from-dotenv --dry-run
Apply the reviewed importshine env workspace init --from-dotenv

The command reads .env, .env.local, .env.<mode>, and .env.<mode>.local, discovers modes, and preserves that precedence. Source dotenv files are unchanged. Existing targets are not overwritten unless you add --force, including dangling symbolic links and files created by a concurrent import. Import selected modes with repeatable --mode:

shine env workspace init --from-dotenv --mode development --mode production

--dry-run previews the import without creating or replacing files.

Mark known sensitive keys for [secret], then configure recipients and seal. Unmarked values are imported as plaintext; never accidentally commit credentials as ordinary configuration. Imported secrets also remain plaintext until sealing. On Unix, generated workspace and source files use owner-only 0600 permissions, including replacements with --force.

shine env workspace init --from-dotenv --secret DATABASE_URL
shine env secret seal

To preserve dotenv semantics, values containing $ (including $BASE_URL and ${BASE_URL}) or escaped double-quoted content are rejected, even inside quotes. Resolve interpolation before importing; keep literal dollar signs in a manually authored Shine source instead. Single-line backtick-quoted values preserve literal hashes, quotes, backslashes, and whitespace. Multiline quoted values are rejected. An unquoted # starts a comment; quote values that contain a literal #. The generated file includes a documented empty [secret] table even when no --secret is selected.

Export a workspace to dotenv​

Export one fully resolved mode when another tool needs a conventional dotenv file, or when you want to stop using Shine env:

shine env workspace export \
--format dotenv \
--mode production \
--output .env.production.local

--format is required so the export contract remains explicit. The command merges the workspace sources in their declared order, but does not include inherited process variables or --with values. By default it exports only winning [plain] entries and does not decrypt payloads. If a later secret declaration shadows an earlier plain value, that old plain value is omitted.

Include sealed secrets only when the destination really needs a complete runnable environment:

shine env workspace export \
--format dotenv \
--mode production \
--output .env.production.local \
--include-secrets

This writes plaintext secrets. Keep the output out of version control; on Unix, it is readable only by its owner. Existing destinations are preserved unless you add --force to replace them. --dry-run previews the destination and non-secret variables without printing values, decrypting secrets, or writing files, even when --include-secrets is present.

The exported file has no Shine metadata or runtime dependency. To leave Shine env, export and test each required mode, add secret-bearing outputs to .gitignore, remove shine env run wrappers, and only then archive or delete shine.workspace.toml and its *.shine.toml sources yourself. Export never deletes those source files.

Export preserves literal backslashes, quotes, and newlines using dotenv quoting. Values containing $, carriage returns, NUL bytes, or no safe quote delimiter are rejected before writing or replacing the output. For those values, continue using shine env run, or prepare an output specifically for your target tool. Always test the exported file with that tool before switching over.

Use layered project environments​

At the project root, declare modes, ordered sources, and shared recipients in shine.workspace.toml:

version = 2

[env]
modes = ["development", "production"]
default_mode = "development"
files = [
".env.shine.toml",
".env.local.shine.toml",
".env.{mode}.shine.toml",
".env.{mode}.local.shine.toml",
]

[env.encryption]
gpg_recipients = ["user@example.com", "team-backup@example.com"]
# For age, uncomment and add every member recipient:
# backend = "age"
# age_recipients = ["age1tag1qexample...", "age1qteammate..."]

Later files override earlier ones. {mode} expands from --mode, or from default_mode when the flag is omitted. A source can contain plaintext and secrets pending sealing:

version = 1

[plain]
VITE_APP_NAME = "Example App"

[secret]
DATABASE_URL = true
API_TOKEN = false
SENTRY_TOKEN = "<value to seal>"

[payload]
data = "<GPG ciphertext managed by Shine>"
  • true keeps an existing value in the encrypted payload.
  • false prompts safely during the next seal.
  • A string is replaced by true after sealing so plaintext does not remain in the file.

Seal pending values and start a command with the merged environment:

shine env secret seal
shine env run --mode production -- bun run build

By default, seal processes workspace sources. Pass a file to seal only that source, or use --workspace <FILE> for another workspace. -r/--recipient temporarily overrides recipients.

Existing process variables override the workspace by default. With env.override_process_env = true, workspace values win. Explicit --with always has highest precedence.

Ignore personal overrides:

.env.local.shine.toml
.env.*.local.shine.toml

Never commit a source containing unsealed strings. Before committing, inspect [secret] and confirm every sealed entry is true. See the configuration reference for file formats and precedence.

Share a payload across GPG and age members​

Before converting a workspace from Shine 2.0.x, upgrade all readers, including the local SSH broker machine, to a release supporting hybrid encryption. Older readers cannot read hybrid: ciphertext. Sealing requires GnuPG 2.2–2.5 and age 1.3+; readers need only the backend they select. Existing untagged GPG and age: ciphertext stay readable; ordinary reads never migrate files. Explicitly configure the workspace (replace the public placeholders):

[env.encryption]
backend = "hybrid"
gpg_recipients = ["<full 40-hex primary fingerprint>"]
age_recipients = ["age1..."]

Run shine env secret seal --workspace shine.workspace.toml. Reading old ciphertext requires its own identity; resealing requires both encryption tools and complete public lists. Either group independently reads the same data ciphertext. Hybrid GPG wrapping excludes local option files and implicit extra recipients; groups and fuzzy user IDs are rejected before existing secrets are decrypted.

Shine may cache the merged environment in encrypted form, but does not save an unlocked session. Backend authorization still applies each time. If decryption fails or you cancel authorization, the command stops.

Changes to workspace inputs invalidate the cached environment. Older cache versions are rebuilt from the sources, which may require authorizing each source again on the first run after upgrading; existing encrypted source files do not need to be resealed.

Hybrid key decryption suppresses backend diagnostics by default. Phone QR prompts and explicitly enabled phone messages remain visible as they arrive. Diagnostic output is limited to 64 KiB per key decryption, including these interactive messages; exceeding the limit stops the operation without trying another backend.

For a personal project preference, create shine.config.local.toml alongside the selected shine.workspace.toml (also when using --workspace):

hybrid_decrypt_backend = "age"

Add /shine.config.local.toml to that project's .gitignore. Local env run, env secret seal, and env workspace export use this preference. An absent file or field inherits global config.toml; without either preference, existing capability checks and interactive selection apply. Invalid configuration fails explicitly. The file currently accepts only hybrid_decrypt_backend = "gpg" | "age". It is trusted local project configuration, so a program able to edit it can change the chosen authorization path. SSH broker requests do not load or transmit this file; the decrypting broker machine retains its own configured choice and release checks.

Seal regular source files, not symbolic links or, on Unix, files with multiple hard links. Do not edit workspace or source files while sealing, including while approving hardware prompts. If sealing fails, earlier files may already be updated and pending plaintext may remain. Inspect the reported files before retrying or committing. Do not delete .shine-seal.lock while sealing is active.

Temporary secret files​

Before enabling temporary secret files, upgrade all clients reading the same workspace to Shine 2.5 or later.

Use workspace version 3 to keep signing keystores, password files, and service credentials encrypted in Git. A file group is a named collection of mappings, not a directory. Each mapping declares a ciphertext source and a plaintext target; one group may span several directories, and repeatable --file-group selects multiple groups. Application code reads ordinary files and does not need a Shine API. Only the wrapped build workflow requires the Shine CLI.

Replace the recipient placeholder with a real public recipient. Identities stay outside the repository:

version = 3

[env]
default_mode = "staging"
modes = ["staging", "production"]

[env.encryption]
backend = "age"
age_recipients = ["<actual age recipient>"]

[[file_groups.android-signing.files]]
source = ".secrets/{mode}/release.jks.enc"
target = ".android-signing/release.jks"

[[file_groups.android-signing.files]]
source = ".secrets/{mode}/signing.properties.enc"
target = ".android-signing/signing.properties"

[[file_groups.service-account.files]]
source = ".secrets/{mode}/service-account.json.enc"
target = "app/.credentials/service-account.json"

Both paths are relative to the workspace directory and support {mode}. --mode overrides env.default_mode; the selected mode must be declared in env.modes or env.default_mode. Missing files fail without falling back to another environment. All modes use the same mapping list. Version 2 workspaces remain supported; older clients reject version 3. A version 3 workspace with file groups may omit env.files; such a workspace requires --file-group for env run.

Ignore all plaintext targets before use in a Git repository, for example .android-signing/ and app/.credentials/. Tracked or non-ignored targets are rejected, including case or Unicode aliases of tracked paths whose working files are missing. Shine does not edit .gitignore. Checks use each target's own repository, including nested repositories. Git inspection failures stop the operation before plaintext is released. File groups also work outside Git repositories, including directories on separate filesystems. Use the following commands after preparing the plaintext files:

shine env secret seal --mode production --file-group android-signing --file-group service-account --dry-run
shine env secret seal --mode production --file-group android-signing --file-group service-account --remove-plaintext
shine env run --mode production --file-group android-signing --file-group service-account -- pnpm android:build

seal --file-group reads target and creates or updates source. It reuses existing GPG, age, and hybrid policies, including CLI overrides and hybrid recipient restrictions. By default it retains plaintext. --remove-plaintext decrypts and compares every written ciphertext before starting plaintext deletion, so it needs a usable identity and may prompt for authorization. Verification or cancellation before deletion retains all original plaintext. If writing or deleting several files fails partway through, earlier changes remain; inspect the reported completed and remaining files. There is no ciphertext-only recipient rotation workflow in this first version.

To keep decrypted files for ordinary tools without wrapping each command in Shine, export the selected groups to their configured target paths:

shine env workspace export --format files --mode production --file-group android-signing --file-group service-account --dry-run
shine env workspace export --format files --mode production --file-group android-signing --file-group service-account

--mode is required. This exports only the selected groups; it does not export environment variables. All files must decrypt successfully before target creation starts. Existing targets are always rejected, including during concurrent writes; inspect and move them before retrying. There is no --force, --output, or --include-secrets option for the files format. Workspace and ciphertext files remain unchanged. The same path, Git exclusion, size, and private-permission requirements apply.

Successful exports remain on disk until you manage or remove them yourself. env run --file-group will refuse these existing targets and will not clean them up. If export fails or is interrupted, Shine attempts to remove files and empty directories created by that export, preserves replacements, and reports cleanup failures and possible leftovers. An interrupted export can leave some files behind, especially after force-killing Shine or losing power. Do not commit the plaintext.

env run --file-group starts the command only after all selected files are decrypted and ready. It never overwrites existing targets. After the command ends or is interrupted normally, Shine attempts to remove the files it created; files replaced during the run are preserved and reported. Inspect any cleanup warnings for plaintext left behind.

Temporary files are restricted to the current user. Windows requires a filesystem supporting private user permissions. Command output remains available for piping, and failures are reported. Do not start detached background tasks that need these files after the command ends; remaining managed child processes are terminated.

--dry-run inspects configuration and metadata only: it does not read plaintext, decrypt, write files, or execute the command. It cannot verify ciphertext integrity or private-key availability. --file-group conflicts with seal <FILE>. For env run, it also conflicts with --no-workspace and --secret-broker; persistent file export supports --secret-broker. Plain seal and env run do not implicitly select file groups.

Paths must stay inside the workspace; absolute paths, traversal, symbolic links/reparse points, hard links, and overlapping mappings are rejected. Mappings cannot overlap the workspace definition, including names that differ only in case or Unicode normalization. These aliases are conservatively rejected across platforms. Each plaintext file is limited to 16 MiB, with 64 MiB per invocation. Encoded ciphertext is limited to 32 MiB per file and 128 MiB per invocation. File ciphertext uses Shine's existing tagged storage encoding, not a native .age or .gpg file.

The child and other processes with sufficient local permissions can read the temporary plaintext. Force-killing Shine or losing power can leave files behind: inspect existing targets manually before retrying. Removing files is not secure erasure. Builds never automatically encrypt modified targets back into the repository. Removing a recipient does not revoke access to ciphertext in Git history.

Export through the SSH broker​

Add --secret-broker to env workspace export --format files to request local authorization and decryption from an enabled shine ssh --secret-broker session. This option requires explicit file groups and a matching file policy; it is unavailable for dotenv export. Without the flag, manual export continues to use the executing machine's backend. See SSH file authorization and automatic App secret installation.