Skip to content

File Provider

The file provider reads and writes one plaintext UTF-8 file per secret.

Providerfile (0.19+)
URIfile:ROOT
AccessRead, write, and delete
Best forLocal fixtures and file-mounted secrets
AuthenticationFilesystem permissions
AvailabilityBuilt in (0.19+)
Storage rootRequired; relative to secretspec.toml or absolute

Choose a directory, exclude it from version control, and route a declaration to it:

.gitignore
/.secrets/
secretspec.toml
[providers]
local_files = "file:./.secrets"
[profiles.development]
API_TOKEN = { description = "Local API token", providers = ["local_files"] }
Terminal window
$ secretspec set API_TOKEN --profile development
$ secretspec get API_TOKEN --profile development
$ secretspec run --profile development -- npm start

The stored value is .secrets/<project>/development/API_TOKEN, where <project> is [project].name from the manifest.

The provider has no external dependency or credential. The SecretSpec process needs read access to the configured directory and write access for set, generated values, imports, cache writes, and deletes.

Relative roots resolve from the directory containing secretspec.toml, not from the shell’s current directory. Every file provider URI must include a root; the bare file provider name is rejected.

file:ROOT

ROOT is required and names the directory that contains the project/profile tree. It can be relative, absolute, or home-relative. The URI accepts no query options or user information.

file:./.secrets # .secrets beside secretspec.toml
file:///var/lib/app/secrets # Absolute directory
file:~/.local/share/myapp # Home-relative directory

Use file:./... for relative paths containing spaces. SecretSpec percent- encodes the path when reporting the provider URI.

Check in the provider alias, but never the directory’s plaintext contents:

secretspec.toml
[providers]
local_files = "file:./.secrets"
[profiles.default]
DATABASE_URL = { description = "Database connection string", providers = ["local_files"] }

The alias is shared configuration. Each machine supplies its own directory contents.

Convention addresses use this path beneath ROOT:

{project}/{profile}/{key}

Project and profile are therefore isolated even when one user-global file provider serves several projects. Each component must be one safe filename; slashes, backslashes, . and .. are rejected in convention components.

Values are read and written exactly as UTF-8 text. Leading and trailing whitespace, newlines, CRLF line endings, and multiline content are preserved. Binary files are rejected by the provider’s text interface; use a SecretSpec 0.19+ encoding when the stored file should contain a textual encoding of binary data.

Writes use a temporary file in the destination directory and atomically replace the entry after flushing it. On Unix, SecretSpec creates new directories with mode 0700 and new entry files with mode 0600. Existing directory permissions are not changed.

A native ref.item is a relative path beneath ROOT. It replaces the entire {project}/{profile}/{key} convention path:

secretspec.toml
[providers]
runtime_files = "file:///run/secrets"
[profiles.production]
DATABASE_PASSWORD = {
description = "Database password mounted by the runtime",
providers = ["runtime_files"],
ref = { item = "database/password" }
}

This reads /run/secrets/database/password. item may contain nested relative components, but absolute paths, empty components, ., .., backslashes, and symbolic links inside the configured root are rejected. field, vault, section, and version do not have file equivalents and are rejected.

Referenced files are writable when filesystem permissions allow it. Treat runtime-managed mounts as read-only unless their owner explicitly permits SecretSpec to replace or delete entries.

Several declarations can select values from one JSON file without making JSON part of the provider itself:

secretspec.toml
[providers]
runtime_files = "file:///run/secrets"
[profiles.production]
# extract is available in SecretSpec 0.19+
DATABASE_USER = {
description = "Database user",
providers = ["runtime_files"],
ref = { item = "application.json" },
extract = { format = "json", pointer = "/database/user" }
}
DATABASE_PASSWORD = {
description = "Database password",
providers = ["runtime_files"],
ref = { item = "application.json" },
extract = { format = "json", pointer = "/database/password" }
}

The file provider returns the complete UTF-8 document; SecretSpec then applies the RFC 6901 pointer as a provider-independent stored-value transform. Extracted declarations are read-only in 0.19 so set, delete, generation, prompting, and import cannot overwrite or remove the containing file. See Structured Extraction for value rendering, error behavior, and composition with encoding.

Use a read-only mounted directory with explicit refs when the runtime already publishes one file per secret. For example, mount the source at /run/secrets and use file:///run/secrets as the provider alias. SecretSpec reads only the files declared by the active profile; it does not enumerate or inject every file in the mount.

The ordinary convention is useful when SecretSpec owns the directory. Give each job a private root and let {project}/{profile}/{key} keep jobs and environments separate.

SecretSpec rejects symbolic links within the configured store so an entry cannot deliberately redirect reads or writes outside its root. It also rejects lexical traversal through ref.item. The configured root itself remains the operator’s trust boundary: protect it with filesystem ownership and mount permissions, and do not let untrusted processes modify it while SecretSpec is running.

Deleting an entry removes only its file. Empty project and profile directories remain in place.