> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://rivet.chrisboulton.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://rivet.chrisboulton.com/_mcp/server.

# File

> Files, directories, symlinks, ownership, safe replacement, and protected backups.

`file` manages a regular file, directory, or symlink at an absolute path.

```hcl
file "directory" {
  path   = "/var/lib/app"
  ensure = "directory"
  mode   = "0755"
}

file "config" {
  path     = "/var/lib/app/config.json"
  content  = "{\"enabled\":true}\n"
  owner    = "root"
  group    = 0
  mode     = "0640"
  backup   = true
  validate = ["/usr/bin/python3", "-m", "json.tool", "{candidate}"]
}
```

A declared parent directory is always ordered before the files inside it, so `depends_on` is only needed for other prerequisites.

| Attribute        | Default               | Behavior                                                                                                     |
| ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------ |
| `path`           | Required              | Absolute path, without `..`.                                                                                 |
| `ensure`         | `file`                | `file`, `directory`, `symlink`, or `absent`. `present` means `file`.                                         |
| `content`        | Unmanaged             | File content as a string. Can't be combined with `source`.                                                   |
| `source`         | Unset                 | A file in the configuration directory, or an `s3://` or `gs://` object. See [assets](/language/assets).      |
| `checksum`       | Unset                 | `sha256:` followed by 64 lowercase hex digits. Requires `source`; required for cloud sources.                |
| `mode`           | Unmanaged             | Octal string such as `"0644"`. New files get 0644 and new directories 0755.                                  |
| `validate`       | Unset                 | Command, as a list of arguments, including `{candidate}`. See [candidate validation](#candidate-validation). |
| `target`         | Required for symlinks | Link target, absolute or relative.                                                                           |
| `owner`, `group` | Unmanaged             | Local account name or numeric ID.                                                                            |
| `create_parents` | `false`               | Create missing parent directories, owned by the runner with mode 0755.                                       |
| `replace_type`   | `false`               | Allow replacing an existing object of a different type.                                                      |
| `backup`         | `false`               | Back up an existing file before replacing or removing it.                                                    |

Which attributes apply depends on `ensure`:

* `content`, `source`, and `validate` require `ensure = "file"`. Without content or source, an existing file keeps its bytes and a new one is empty.
* `target` is required for, and only valid with, `ensure = "symlink"`. Symlinks can't have content or a mode.
* `ensure = "absent"` can't manage ownership, mode, parents, or type; `backup` still applies.

Optional attributes accept `null` as "not set". `path`, `ensure`, and the boolean policies don't.

## Ownership, parents, and links

```hcl
file "settings" {
  path           = "/etc/example/settings"
  content        = "enabled=true\n"
  owner          = "root"
  group          = 0
  mode           = "0640"
  create_parents = true
  backup         = true
}

file "current" {
  path           = "/opt/example/current"
  ensure         = "symlink"
  target         = "releases/current"
  create_parents = true
  replace_type   = true
}
```

If you don't set `owner` or `group`, the current ones are kept, even when the content is replaced. Owners and groups managed in the same configuration are ordered before the file. Changing only ownership or mode updates the object in place. Relative symlink targets are written exactly as given, and Rivet never follows a symlink at the managed path itself.

Existing parent directories are left as they are. Rivet refuses to work through a parent directory that is a symlink, or that someone other than root, the runner, the file's owner, or the owning account (for paths in its home directory) can write to, unless the directory is sticky like `/tmp`. When a directory and files inside it are both removed, the files go first.

## Candidate validation

With `validate`, Rivet writes the new content to a private temporary file next to the destination, then runs the validator with `{candidate}` replaced by that file's path. The validator must exit 0 and must not modify the file. Only then is the file moved into place; if validation fails, the temporary file is removed and the destination is untouched. Rivet also checks that the destination didn't change while the validator ran.

Plan and noop don't run validators. Validators run with the runner's authority and aren't sandboxed, so they must only read.

## Replacing a different type

Changing an existing object to another type, for example a file to a symlink, requires `replace_type = true`. Special files and non-empty directories are never replaced, and Rivet never deletes recursively. Files and symlinks are replaced atomically. Converting a directory involves a brief gap between removal and creation, which the journal covers if the run is interrupted.

## Safety checks

Rivet verifies each change before reporting it, and leaves objects that already match alone. It refuses to:

* overwrite an object that appeared concurrently while creating a file;
* change a file whose extended attributes, ACLs, or security labels it can't preserve;
* replace or change metadata of a regular file with more than one hard link. `ensure = "absent"` removes only the managed name.

If a timeout interrupts a change after ownership or mode was already applied, the result is reported as a partial change and the journal keeps the work for [recovery](/reference/recovery). There is no attribute for managing ACLs.

## Backups

With `backup = true`, Rivet copies an existing regular file before replacing its content or type, or removing it. If the backup fails, the replacement doesn't happen. Backups are stored in `/var/lib/rivet/backups` (mode 0700, files 0600) even when `--state-dir` points elsewhere. Each backup records the original path, ownership, permissions, and digest.

Backups are kept until you delete them and may contain secrets. Rivet never restores them automatically.

## Capabilities and timeout

The default timeout is 60 seconds. Files and directories need `file.native.v1`. Ownership, symlinks, `create_parents`, `replace_type`, and `backup` need `file.complete.v1`; owner or group names need `file.ownership.nss.v1`; and `validate` needs `file.validate.v1`. See [capabilities](/reference/capabilities) and [assets](/language/assets).