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:
| Operation | Command |
|---|---|
| List variables and their sources | shine env list |
| Read the effective value | shine env get HTTP_PROXY_PORT |
| Inspect precedence and sources | shine env explain HTTP_PROXY_PORT |
| Save a value | shine env set HTTP_PROXY_PORT 6152 |
| Delete the selected stored value | 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.
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:
| Options | Target |
|---|---|
| None | Active project shine.config.toml [env], otherwise global config |
--global | Global config.toml [env] |
--project | Nearest project [env]; fails without an existing project |
--env-file | Selected or default scope's shine.env.toml |
--overlay | Active 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:
| Operation | Command |
|---|---|
| Write global configuration | shine env set HTTP_PROXY_PORT 7890 --global |
| Write project configuration | shine env set HTTP_PROXY_PORT 8080 --project |
| Write the project env file | shine env set HTTP_PROXY_PORT 8081 --project --env-file |
| Write the manual overlay | shine env set HTTP_PROXY_PORT 6152 --overlay |
| Delete from the global env file | shine 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 file | shine 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.
- 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.
- Replace old
--forcescripts: use--global --env-filefor global env files,--project --env-filefor project env files, and--overlayfor manual overlays. Maintain managed mirror values upstream or select local overrides. - After upgrading, run
env listandenv explain KEYoutside projects and in each important project. Projects can select different overlays; one project's result does not describe another. - 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.
- Check
KEY_SECRET: changing or deleting plaintext does not cancel encrypted-first injection. Runshine update, review theshine upgradePlan, 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:
| Operation | Command |
|---|---|
| Preview legacy-state migration | shine state migrate --dry-run |
| Apply after reviewing the preview | shine 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:
| Operation | Command |
|---|---|
| Inject under the same name | shine env run --with MY_TOKEN -- bun run build |
| Rename for the child process | shine env run --with MY_TOKEN=API_TOKEN -- bun run build |
| Inject multiple variables | shine env run --with TOKEN_A --with TOKEN_B=OTHER_TOKEN -- bun run build |
| Skip workspace loading | 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 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:
| Operation | Command |
|---|---|
| Disable the proxy | shine env proxy disable gh |
| Enable the proxy | shine env proxy enable gh |
| Disable the project proxy | shine 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:
| Goal | Local command | Plaintext visibility |
|---|---|---|
| Forward a normal value | shine ssh --with API_URL dev | Remote login shell or specified command |
| Decrypt and forward a secret directly | shine ssh --with-secret API_TOKEN dev | Remote login shell or specified command |
| Let an authorized remote child request local decryption | shine ssh --secret-broker ... dev | Only 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:
| Operation | Command |
|---|---|
| Preview importing dotenv files | shine env workspace init --from-dotenv --dry-run |
| Apply the reviewed import | 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
--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>"
truekeeps an existing value in the encrypted payload.falseprompts safely during the nextseal.- A string is replaced by
trueafter 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.