---
name: write
description: Replace a file atomically, only if it holds what the caller says it holds (--if-sha256) or does not exist (--create); idempotent, locked, with --dry-run.
---

# write 0.1.0

Replace a file atomically, only if it holds what the caller says it holds (--if-sha256) or does not exist (--create); idempotent, locked, with --dry-run.

```
write [--root DIR] (--create | --if-sha256 HEX) [--content-sha256 HEX] [--dry-run] [--max-bytes N] [--max-diff-lines N] (--stdin | --content-file PATH) [--format json|text] PATH
```

## Output

One JSON object on one line, schema `write.v1`: `{ok, command, schema, data, error, errors, meta}`. `ok` is false exactly when `errors` is non-empty; `error` is the first.

An error is `{code, rule, message, hint, repair, detail}`. Match on `rule`, not on `message`. When `repair` is `{"kind":"retry","argv":[...]}` the argv can be run as it stands; it never widens what the tool may touch.

`--format text` is for a person and is lossy; do not parse it.

## Flags

- `--root <path>`: resolve PATH and --content-file relative to this directory and refuse paths outside it
- `--create`: the path must not exist yet (if it holds exactly the new content, nothing is written and changed is false)
- `--if-sha256 <hex64>`: the file's current content must hash to this SHA-256
- `--content-sha256 <hex64>`: the new content must hash to this SHA-256, checked before anything is written
- `--dry-run`: check everything and report planned_actions, exit 9, write nothing
- `--stdin`: read the new content from standard input
- `--content-file <path>`: read the new content from this file
- `--max-bytes <nat>`: the largest new content accepted, and the largest old content diffed (ceiling 1073741824) Default `67108864`.
- `--max-diff-lines <nat>`: the most changed lines the diff shows (ceiling 100000) Default `200`.
- `--format <choice:json/text>`: json for a program, text for a person Default `json`.

An unknown flag is refused (`args.unknown-flag`), never ignored. POSIX spellings are not promised.

## Guarantees

- **deterministic**: the same input gives byte-identical output, through a pipe, a file or a terminal (M2)
- **idempotent**: running it twice leaves the same state as once (M7 for the writers, and a reader changes nothing)
- **atomic**: a reader never sees a partial target: the new content is a synced temporary renamed over it (M7)
- **requires_precondition**: it refuses to change a file without a stated belief about it: --create, --if-sha256 or --expect (M3)
- **dry_run**: --dry-run reports what an apply would do and makes no mutating system call (M7, under strace)
- **concurrency**: locked; of writers racing with the same precondition, exactly one wins and the others are refused, so re-read and retry.

## Exit codes

- `0` SUCCESS: what was asked was done; zero matches is success
- `1` GENERAL_ERROR: the operating system failed mid-operation, or the tool caught a bug in itself
- `2` INVALID_ARGS: the invocation is malformed or names something of the wrong kind; nothing was read
- `3` NOT_FOUND: a named input does not exist
- `4` PERMISSION_DENIED: the operating system refused, or the tool's own confinement did
- `5` CONFLICT: state is not what the caller said it would be
- `8` PRECONDITION_FAILED: the answer is no, or a limit was reached
- `9` DRY_RUN: a --dry-run completed; planned_actions lists what an apply would do
- `132`: the tool trapped. A bug in the tool; there is no JSON.
- `141`: the reader closed the pipe.

## Rules

- `args.unknown-flag` (exit 2): a flag that is not in the tool's table
- `args.missing-value` (exit 2): a flag that takes a value was given none
- `args.bad-value` (exit 2): a flag's value is not of the flag's kind
- `args.unexpected-value` (exit 2): a flag that takes no value was given one
- `args.duplicate-flag` (exit 2): the same flag was given twice
- `args.conflict` (exit 2): two flags that exclude each other were both given
- `args.required-flag` (exit 2): a flag the tool needs was not given
- `args.missing-operand` (exit 2): an operand the tool needs was not given
- `args.too-many-operands` (exit 2): more operands than the tool takes
- `path.empty` (exit 2): an empty path
- `path.dotdot` (exit 2): a path with a `..` component
- `path.absolute` (exit 2): an absolute path where one relative to --root is wanted
- `path.outside-root` (exit 4): an absolute path that is not under --root
- `path.too-long` (exit 2): a path longer than 4096 bytes
- `path.symlink` (exit 4): a path that runs through a symbolic link, which the tool does not follow
- `io.not-found` (exit 3): a named input does not exist
- `io.not-a-directory` (exit 3): a path runs through something that is not a directory
- `io.is-a-directory` (exit 2): a directory where a file is wanted
- `io.permission-denied` (exit 4): the operating system refused access
- `io.read-failed` (exit 1): the operating system failed a read
- `io.write-failed` (exit 1): the operating system failed a write, or a write was short
- `io.rename-unsupported` (exit 8): the filesystem cannot refuse an existing name atomically, so move did not rename
- `limit.input-too-large` (exit 8): an input longer than --max-bytes
- `precondition.required` (exit 2): a write that states no belief about the file
- `precondition.hash-mismatch` (exit 5): the file's hash is not the one --if-sha256 named
- `precondition.content-mismatch` (exit 5): the new content's hash is not the one --content-sha256 named
- `conflict.exists` (exit 5): --create was given and the path exists
- `conflict.locked` (exit 5): another writer holds the path's lock

## Authority

What the compiler says this program can reach (`cancho authority`), embedded at build time:

```json
{"bounded":true,"unbounded_by":[],"effects":["args","dir_read","dir_write","err_write","file_read","file_write","fs_read","heap","io_read","io_write"],"labels":[{"name":"args","argument":null,"bounded":true},{"name":"dir_read","argument":null,"bounded":true},{"name":"dir_write","argument":null,"bounded":true},{"name":"err_write","argument":null,"bounded":true},{"name":"file_read","argument":null,"bounded":true},{"name":"file_write","argument":null,"bounded":true},{"name":"fs_read","argument":"","bounded":true},{"name":"heap","argument":null,"bounded":true},{"name":"io_read","argument":null,"bounded":true},{"name":"io_write","argument":null,"bounded":true}],"never_touches":["the network","signals","other programs","foreign code"],"foreign_symbols":[],"pure":["alphabet","authority","budget","built","bytes","catalogue","ceiling","code_meaning","code_name","compiler","count","count","count","count_byte","decode","default_of","diff_ceiling","digit_of","discard","distance","emits","enotdir","entries","entry","entry","entry_at","entry_end","entry_in","equal","err_control","err_depth","err_end","err_escape","err_full","err_number","err_syntax","err_trailing","err_utf8","escape_width","exit_code","exit_in_entry","exit_of_in","expected","field","field_of","field_of","find","find_in","finish","first","flag_table","full","get$int","guarantee_keys","guarantees_has","has","has_dotdot","hex4","hex_digit","hex_digit","hex_value","in_choice","in_range","index_of","index_of_short","io_rule","is_ascii","is_digit","is_digit","is_directory_name","is_flag","is_hex","is_loop","is_named","is_tail","is_utf8","is_valid","is_ws","item","kind_of","lead_bits","leaf","line_count","listed","longest","low_mask64","lshr64","mask32","max_depth","message_of","name_of","nearest","ok","operand_count","operand_index","parent_of","parse_nat","plain_byte","prefix","rename_unsupported","repairable_in","rotr64","rule_of","scan_for","schema","second_range","sentences","shell_builtin","shown","size","size$int","slash","start","starts_line","starts_with","string_end","suffix","summary_in","takes_value","tape_len","tool","under","update","valid","value_index","width_of","would_block"],"folded_operators":29,"folded_calls":0,"functions":252}
```

The row names *kinds* of access, not *extent*: a path from the command line is checked at run time, so it reads `fs_read("")`. Symlinks are followed.
