cancho-tools

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 document
schema
write.v1
reversibility
irreversible-bounded
guarantees
deterministic, idempotent, atomic, requires_precondition, dry_run
concurrency
locked: of writers racing with the same precondition, exactly one wins and the others are refused (M7)
confinement
beneath: with --root, paths are checked lexically against it and then opened beneath it one component at a time, following no symbolic link (path.symlink), so nothing outside the root is reachable through one; without it, a reader opens the path as given, links and all, and a writer still follows no link in the file's own name
reads the environment
no
reads the clock
no
compiler
de4e52fd8180
for an agent
SKILL.md (write skill)

Authority

Derived by cancho authority from the source, embedded in the binary, and checked against tools.toml's ceiling in CI. Every label is bounded.

What the row cannot say:

Operands

OperandRoleHow manyMeaning
PATHpath-write1the file to create or replace

Flags

FlagKindRoleDefaultMeaning
--rootpathrootresolve PATH and --content-file relative to this directory and refuse paths outside it
--createboolguardthe path must not exist yet (if it holds exactly the new content, nothing is written and changed is false)
--if-sha256hex64guardthe file's current content must hash to this SHA-256
--content-sha256hex64nonethe new content must hash to this SHA-256, checked before anything is written
--dry-runboolguardcheck everything and report planned_actions, exit 9, write nothing
--stdinboolnoneread the new content from standard input
--content-filepathpath-readread the new content from this file
--max-bytesnatnone67108864the largest new content accepted, and the largest old content diffed (ceiling 1073741824)
--max-diff-linesnatnone200the most changed lines the diff shows (ceiling 100000)
--formatchoice:json/textnonejsonjson for a program, text for a person

Limits

LimitDefaultCeiling
max-bytes67,108,8641,073,741,824
max-diff-lines200100,000

Exit codes

CodeNameMeaning
0SUCCESSwhat was asked was done; zero matches is success
1GENERAL_ERRORthe operating system failed mid-operation, or the tool caught a bug in itself
2INVALID_ARGSthe invocation is malformed or names something of the wrong kind; nothing was read
3NOT_FOUNDa named input does not exist
4PERMISSION_DENIEDthe operating system refused, or the tool's own confinement did
5CONFLICTstate is not what the caller said it would be
8PRECONDITION_FAILEDthe answer is no, or a limit was reached
9DRY_RUNa --dry-run completed; planned_actions lists what an apply would do

Rules (28)

Match on rule, not on the message. A retry repair is an argv that can be run as it stands, and never widens what the tool may touch.

RuleExitRepairWhen
args.unknown-flag2sometimesa flag that is not in the tool's table
args.missing-value2nevera flag that takes a value was given none
args.bad-value2nevera flag's value is not of the flag's kind
args.unexpected-value2nevera flag that takes no value was given one
args.duplicate-flag2neverthe same flag was given twice
args.conflict2nevertwo flags that exclude each other were both given
args.required-flag2nevera flag the tool needs was not given
args.missing-operand2neveran operand the tool needs was not given
args.too-many-operands2nevermore operands than the tool takes
path.empty2neveran empty path
path.dotdot2sometimesa path with a `..` component
path.absolute2alwaysan absolute path where one relative to --root is wanted
path.outside-root4neveran absolute path that is not under --root
path.too-long2nevera path longer than 4096 bytes
path.symlink4nevera path that runs through a symbolic link, which the tool does not follow
io.not-found3nevera named input does not exist
io.not-a-directory3nevera path runs through something that is not a directory
io.is-a-directory2nevera directory where a file is wanted
io.permission-denied4neverthe operating system refused access
io.read-failed1neverthe operating system failed a read
io.write-failed1neverthe operating system failed a write, or a write was short
io.rename-unsupported8neverthe filesystem cannot refuse an existing name atomically, so move did not rename
limit.input-too-large8sometimesan input longer than --max-bytes
precondition.required2nevera write that states no belief about the file
precondition.hash-mismatch5neverthe file's hash is not the one --if-sha256 named
precondition.content-mismatch5neverthe new content's hash is not the one --content-sha256 named
conflict.exists5never--create was given and the path exists
conflict.locked5neveranother writer holds the path's lock

Evidence

each gate is a test in cancho-tools/tests/conformance, run by CI on the commit this binary was built from:

Agent in the loop: not run: no claim that this tool is more agent-friendly than the incumbents is made (cancho docs/agent-toolbox.md section 7.2).