# File Provider

> Store each secret in one plaintext file beneath a local directory

**New in version 0.19**

The [file](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html) provider reads and writes one plaintext file per secret.

## At a glance

|                |                                                     |
| -------------- | --------------------------------------------------- |
| Provider       | `file` (0.19+)                                      |
| URI            | `file:ROOT`                                         |
| Access         | Read, write, and delete                             |
| Best for       | Local fixtures and file-mounted secrets             |
| Authentication | Filesystem permissions                              |
| Availability   | Built in (0.19+)                                    |
| Storage root   | Required; relative to `secretspec.toml` or absolute |

## Quick start

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

**.gitignore**

```text
/.secrets/
```

**secretspec.toml**

```toml
[providers]
local_files = "file:./.secrets"


[profiles.development]
API_TOKEN = { description = "Local API token", providers = ["local_files"] }
```

```bash
$ 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.

## Setup

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.

## Configuration

### URI format

```text
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.

### URI examples

```text
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.

### Project configuration

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

**secretspec.toml**

```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.

## Storage model

**Changed in version 0.21**

File values are arbitrary bytes. Reads and writes preserve every byte exactly.

Convention addresses use this path beneath `ROOT`:

```text
{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.

In SecretSpec 0.21+, values are read and written as arbitrary bytes. NULs, non-UTF-8 bytes, leading and trailing whitespace, and line endings are all preserved. In 0.21+, `get` and the Rust byte resolution APIs return binary values inline, and `run` preserves non-UTF-8 bytes on Unix (except NULs). Declare `as_path = true` to materialize binary data for an application. Text SDK responses, exports, composition, and extraction require valid UTF-8.

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.

## Use existing files

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

**secretspec.toml**

```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.

## Extract from a document

**New in version 0.19**

**Changed in version 0.20**

Structured extraction now supports INI documents with `format = "ini"`.

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

**secretspec.toml**

```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" }
}
```

An INI file is selected the same way with `format = "ini"` (0.20+), where `/key` reads an unsectioned key and `/section/key` reads a key in a named section:

**secretspec.toml**

```toml
[profiles.production]
# format = "ini" requires SecretSpec 0.20+
DATABASE_PASSWORD = {
  description = "Database password",
  providers = ["runtime_files"],
  ref = { item = "application.ini" },
  extract = { format = "ini", pointer = "/database/password" }
}
```

The file provider returns the complete document; SecretSpec requires UTF-8 and then applies the pointer as a provider-independent stored-value transform. Extracted declarations are read-only so `set`, `delete`, generation, prompting, and import cannot overwrite or remove the containing file. See [Structured Extraction](/reference/configuration/#structured-extraction-019) for value rendering, error behavior, and composition with `encoding`.

## CI/CD and containers

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.

## Security considerations

> **Plaintext storage**
>
> The file provider does not encrypt values. Keep its root out of version control, backups, build artifacts, container layers, and shared directories. Use an encrypted provider when the filesystem is not an acceptable trust boundary.

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.