Skip to main content
Version: Next

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​

shine env list
shine env get HTTP_PROXY_PORT
shine env explain HTTP_PROXY_PORT
shine env set HTTP_PROXY_PORT 6152
shine 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. List cells escape line breaks, tabs, other control characters and backslashes, and truncate to terminal display width. These display changes do not alter stored values or env get output. 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. List/explain paths and env-write/proxy-toggle receipts escape terminal control characters while preserving stored paths. For control-bearing keys, explain requests the original key instead of printing a copyable command with a different spelling.

Defaults are runtime fallbacks: queries create no configuration. Setting a value equal to a builtin still records an explicit entry.

Configuration TOML parse errors report file or field context without printing configured values or source lines. Env reads and writes return exit code 1; correct the input and retry. A failed parse does not apply the requested env edit. Workspace definitions and personal shine.config.local.toml preferences also omit document contents from TOML parse errors.

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.

shine env set HTTP_PROXY_PORT 7890 --global
shine env set HTTP_PROXY_PORT 8080 --project
shine env set HTTP_PROXY_PORT 8081 --project --env-file
shine env set HTTP_PROXY_PORT 6152 --overlay
shine env delete HTTP_PROXY_PORT --global --env-file
shine env secret encrypt --from MY_TOKEN --set MY_TOKEN_SECRET --project --env-file

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.

Write preflight runs before encryption tools. Before committing, Shine rechecks the target, configuration sources, and --from input; changes require a retry. A per-target file lock serializes Shine's final validation and atomic replacement. It does not lock every external editor or Git operation. An error saying the file was saved but effective-state verification failed means the write happened: inspect configuration and query again.

On Unix, configuration, env files, and temporary files use owner-only 0600 permissions. A failed atomic replacement preserves the original. Writes reject symlink and special-file targets and preserve comments, unrelated entries, and detailed descriptions. Plaintext remains 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​

Shine handles Base64 encoding and decoding internally for both GPG and age secrets; no external base64 command is required for these operations. Install the selected encryption backend (gpg or age) and any identity plugins it needs. Existing Shine ciphertext needs no migration. Wrapped Base64 and ASCII whitespace are accepted; malformed encoding, including missing or invalid padding, is rejected.

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:

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 with exit code 1 and no export code on stdout; env get and env secret decrypt retain raw output.

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 uses age-keygen and defaults to ~/.shine/age/identity.txt. For a custom --output, any printed age_identity configuration hint uses an absolute path and TOML quoting, so it remains usable from another directory. The hint does not modify your config. On Unix, --output keeps native filename bytes. If the absolute path is not valid UTF-8, Shine omits the copyable age_identity assignment; use a UTF-8 path to configure it in TOML. identity list shows each configured identity path and recipient. On Unix, literal backslashes in filenames are preserved; Windows paths are displayed with forward slashes. Identity lists and software/Touch ID generation result rows escape terminal control characters in paths and recipients. Copyable configuration hints retain TOML quoting, including multiline values. Existing files require --force. Forced replacement generates and checks the new identity in a private temporary directory before replacing the old file; generation or validation failure preserves the old identity. On Unix, the resulting identity file has owner-only 0600 permissions.

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:

shine state migrate --dry-run
shine state migrate

The migration updates configured age1se... values to equivalent age1tag... values without reading identities or decrypting secrets. It checks the global config, current project config, and nearest workspace together; dry-run reports per-file counts, and applying preserves array order, comments, and duplicates. All legacy recipients are validated before any file is written. Reseal afterward to write new ciphertext using the native tagged recipient. Existing ciphertext remains unchanged and decryptable as before.

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. Completed source paths in ordinary seal output display terminal control characters as escapes.

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:

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

Shine leaves all pairing, hardware keys, replay, locator, interruption, and cleanup state under the plugin's ownership. After successful fingerprint confirmation, it adds only the public identity-stub 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"]

Multiple phone identity stubs are valid: age must treat a valid stub that does not match the ciphertext as an ordinary miss and continue to the next configured identity. Older age-plugin-phone builds may instead stop at the first nonmatching stub. If hybrid unwrap fails only while several phone identities are configured, set AGE_PLUGIN_PHONE_MESSAGES=1 to confirm the plugin diagnostic and upgrade the plugin. As a temporary workaround, configure only the matching identity path; do not delete pairing or recovery state merely to narrow this list.

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. A successful shine env secret decrypt writes only the decrypted value, suppresses the age client's own waiting diagnostic, and does not append a line ending. A shell theme may still place its next prompt on a fresh line. 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:

shine env run --with MY_TOKEN -- bun run build
shine env run --with MY_TOKEN=API_TOKEN -- bun run build
shine env run --with TOKEN_A --with TOKEN_B=OTHER_TOKEN -- bun run build
shine 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 runtime and target paths; an unrepresentable path is rejected before launcher, configuration or receipt files are written.

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 an explicitly approved bare command name containing ASCII letters, numbers, -, _, or .. Make sure ~/.shine/bin/ is early in PATH and the target is not another Shine wrapper. Shine refuses to overwrite a same-name entry it does not own. On Windows, it checks the entire launcher set, including .cmd and .ps1, before writing any file. A conflicting file or symbolic link is preserved. PATH entries that resolve to the Shine bin directory, including symbolic-link aliases, are skipped when locating the real command. Proxy rule changes also retain owner-only configuration permissions on Unix. On Unix, target discovery also skips files without execute permission and follows executable symlinks without changing the command's invocation name. On Windows, pass a command name without an executable extension: use gh, not gh.exe. Installation rejects .exe, .com, .bat, .cmd, and .ps1 suffixes, ignoring case. Previously installed suffixed names can still be removed with shine env proxy uninstall <COMMAND>. Shine refuses to proxy shine or shine.exe (case-insensitive) to prevent recursive launcher calls. Uninstall checks every launcher before changing rules or receipts; an ownership conflict leaves the launcher set and configuration unchanged. If a launcher changes while uninstall is running, Shine preserves the changed file and reports an error.

Installation validates the rule file, manifest, and complete launcher set before writing. If a write fails, Shine attempts to restore previous file contents and Unix permissions and removes new files. Concurrent edits are preserved; an incomplete rollback is reported explicitly. This rollback covers reported write failures, not abrupt process termination. TOML parse errors in captured proxy rules or the proxy manifest omit document contents; invalid input still blocks the operation before launcher, rule, or manifest changes.

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

Proxy list fields, installation target notices, and post-save warning details escape terminal control characters without changing saved rules or executable targets.

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

shine env proxy disable gh
shine env proxy enable gh
shine env proxy disable gh --project

Enable/disable output identifies the configuration file that was saved and separately reports the current effective state. Without --project, the global rule changes even inside a project; a project override can therefore keep a different state. If the subsequent state check fails, Shine warns that the rule was saved but its effective state could not be verified; the successful save still returns exit code 0.

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:

shine env workspace init --from-dotenv --dry-run
shine 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

Without --force, previews say Would create and successful writes say Created. With --force, messages describe writing with replacement if a target is present; they do not claim that every target is new. --dry-run never writes or replaces 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. On Unix, a secret-bearing output is created with owner-only 0600 permissions; keep it out of version control on every platform. Without --force, existing outputs (including dangling symbolic links) are preserved, even if they appear while the export is waiting for decryption. Concurrent exports to the same destination allow only one to succeed. Add --force to replace an existing output. --dry-run reports the mode, destination, and effective non-secret variable count without printing values or writing the file. Secrets are not decrypted or counted, even with --include-secrets; that option is applied only during the actual export. A plain value overridden by a secret is excluded from the preview count. Destination paths in previews, success messages, and existing-output errors are quoted and display-escaped: newlines and tabs appear as \n and \t. This changes only the message; the actual destination and exported bytes are preserved.

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.

For single-backend policy and sources, usable recipients let env run store an encrypted, mode-specific cache with the selected backend in the system cache directory. It rebuilds automatically when workspace content, source content, or file order changes. A non-UTF-8 project directory keeps a separate cache from a different directory with its replacement-character spelling. If the cache document or decrypted cache is malformed, Shine warns without showing cached values or document contents and rebuilds from the workspace sources. Cache read/write warnings escape control characters in paths and error text; child output and exit status are preserved.

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.

For hybrid policy or a hybrid source consumed by the current mode, env run stores an encrypted compilation cache using the locally selected GPG or age backend. Only that backend is needed to create the cache. Recipients come from the workspace's corresponding list, never global recipients. An absent list skips caching; a cache write failure warns but does not prevent running successfully compiled values.

A cache hit decrypts the merged environment once. There is no TTL or saved unlock state: backend authorization still applies on every read. Workspace/source content, source order, selected backend, or recipients changing invalidates the cache. GPG and age caches are separate; legacy caches cannot substitute for hybrid sources. Malformed hybrid envelopes fail before cache decryption. Once cache decryption starts, failure or cancellation stops the command without retrying from sources. Export does not use this cache: it omits secrets unless --include-secrets is set, and dry-run never decrypts secrets.

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.

Sealing requires regular source files and rejects symbolic links; on Unix, it also rejects files with multiple hard links. This prevents atomic replacement from leaving plaintext in another linked file. GPG encryption and decryption explicitly use standard output regardless of local output settings; empty encryption output fails without replacing the source.

Sealing locks out cooperating Shine sealers and rechecks captured workspace and source bytes before replacement. Edits during hardware approval abort the current file; earlier completed files remain completed and remaining files are not processed. Do not edit during sealing: editors ignoring the lock can race in the final comparison to replacement interval. Failed sealing may leave pending plaintext entries; Shine does not claim they were cleared. .shine-seal.lock files may remain, contain no secrets, and must not be deleted while a seal is active.

Temporary secret files​

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. 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. Multi-file writes and deletion are not one transaction; failures report 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. Multi-file export is not an atomic transaction; force-killing Shine or losing power can leave files behind. Do not commit the plaintext.

env run --file-group decrypts all selected files before preparing targets, starts the command only after every target is ready, and cleans up its files on success, failure, or controlled interruption. Existing targets are never overwritten. Replaced targets are retained and reported. If the child deletes a target or its parent directory, that target is already cleaned; Shine still cleans the remaining mappings and preserves the child's exit status. Replacement files, directories, and dangling links remain protected and reported. On Unix, cleanup can still remove an unchanged owned file after the child removes its read permission; it never restores permissions or deletes a replacement to make cleanup succeed. Phone-backed age decryption preserves QR authorization and explicitly enabled terminal messages; ordinary captured backend diagnostics remain hidden. env run and file export put mapping previews on stderr so the command's stdout remains byte-exact. Dry-run previews remain on stdout. Mapping previews quote each path and escape control characters such as newlines, keeping each mapping on one line. Seal, plaintext-removal and export result paths, and cleanup warnings also escape terminal controls. These display escapes do not change the paths used for file operations. New Unix files use 0600 and directories use 0700; Windows requires a filesystem supporting private current-user ACLs. Cleanup failure returns an error; a failed child retains its exit code. The wrapper supervises its process group on Unix and a Job Object on Windows. Do not start detached background tasks that outlive the wrapped command; remaining scoped descendants 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>, --no-workspace, and --secret-broker as applicable. 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.