GitHub
View as markdown

lets Manual — transform

lets transform

Structured edits for structured files: JSON, YAML, TOML and markdown frontmatter. Parses, mutates and re-emits, preserving comments, key order and indentation. Replaces jq, yq and sed run against config files.

lets transform [OPTIONS] [FILE]

What it does

--set, --delete and --append take a dotted path (a.b[0].c) and change one value in place without rewriting the rest of the file. The same structural guardrail that edit runs — a real parser for these four formats — runs before and after every change and reverts on failure.

Flags

Flag Meaning Default
--set <path=value> set a key’s value (repeatable) —
--delete <path> remove a key (repeatable) —
--append <path=value> append to an array, or append a table to a TOML array of tables —
--from - a JSONL batch on stdin (- is the only accepted value), one file’s operations per line, validation-atomic like edit --from - —
--if sha:<12+ hex> refuse (exit 5) if the file changed since the hash was taken —
--check <cmd|@preset> same layer-2 checker as edit — see /docs/edit/#checks-the-guardrail —
--check-timeout <secs> timeout before a layer-2 result is inconclusive 60
--no-check skip the structured-format check off
--json, --jsonl, --budget, --max-bytes, --max-file-bytes, --no-ignore, --allow-outside, -q/--quiet shared flags see lets transform --help

Path syntax

  • Dotted keys with numeric indexes: a.b[0].c.
  • --set keeps the existing type. When path already holds a string, a numeric-looking value is still stored as a string; when it already holds a number or bool, the value is parsed as JSON. A brand-new key’s number is written byte-for-byte as typed (--set v=3.10 writes 3.10, not 3.1), since a version string and a decimal look identical on the command line.
  • A quoted key is one literal key, not a path. --set '"editor.formatOnSave"=true' sets the single key literally named editor.formatOnSave, distinct from the nested path editor.formatOnSave.
  • Attribute selectors — plugins[name=gitty].version — select the array element whose named field equals the given value. lets resolves the selector to a plain numeric index before any format-specific parser sees the path, and the footer names the resolution: plugins[name=gitty] → plugins[2]. No match is exit 1, listing the values the field actually held; more than one match is exit 2.
  • --append 'bin[]={"name":"b","path":"b.rs"}' on a TOML array of tables appends a new [[bin]] table after the last one, keeping existing formatting.

Output shape

── <path> · <format> · <op summary> · line(s) <n>
<n>	<context line>
<n>~	<changed line>
<n>+	<inserted line>
<n>-	<deleted line>          (deleted)
── check: <format> ok · sha:<before>→<after>

Exit codes

Same table as edit (exit codes), plus this verb’s own reason for unsupported_file: a file that is not JSON, YAML, TOML or markdown-with-frontmatter, or a key that exists but can’t be changed in place (a tagged YAML node, or one reached through an alias).

Exit Slug Means
0 — applied
1 not_found the path, or an attribute selector’s value, matched nothing
2 ambiguous an attribute selector matched more than one element
3 check_failed the guardrail reverted the change
5 changed the file changed since the --if sha:… given
6 outside_tree the target is outside the working tree
7 unsupported_file not a structured format, or an in-place-uneditable key
64 usage malformed command line, e.g. an out-of-range TOML integer

Examples

Set two JSON values; comments and the rest of the file survive:

$ lets transform config.json --set features.e2e=false --set review.threads=3
── config.json · json · set features.e2e, review.threads · lines 3, 4
1   {
2     // feature flags
3~    "features": { "e2e": false },
4~    "review": { "threads": 3 }
5   }
── check: json ok · sha:bfe3e156b1ce→01c03fe96fd3 · ~21 tokens

Frontmatter is a YAML edit between the fences of a markdown file:

$ lets transform docs/note.md --set last_updated=2026.09.16
── docs/note.md · frontmatter · set last_updated · line 4
2 	title: Notes
3 	tags: [moc]
4~	last_updated: 2026.09.16
5 	---
6 	# Notes
── check: frontmatter ok · sha:5abff77ae5c2→60afc02190e6

An attribute selector that matches two elements is refused, not guessed:

$ lets transform compose.yaml --set 'services[name=web].image=c'
? 2
services[name=web].image is ambiguous (2 candidates)
  compose.yaml:2	services[0].name="web"
  compose.yaml:4	services[1].name="web"
ERROR_CODE=ambiguous

Append a TOML array-of-tables entry, formatting preserved:

$ lets transform Cargo.toml --append 'bin[]={"name":"b","path":"src/b.rs"}'
── Cargo.toml · toml · append bin · line 8
 5 	name = "a"
 6 	path = "src/a.rs"
 7+	
 8+	[[bin]]
 9+	name = "b"
10+	path = "src/b.rs"
── check: toml ok · sha:[..]→[..]

Deleting a key shows it struck out with a - marker, then the surrounding lines:

$ lets transform config/settings.yaml --delete legacy.token
── config/settings.yaml · yaml · delete legacy.token · line 4
1 	allow:
2 	  - ls
3~	legacy: {}
4-	  token: abc123 # remove me          (deleted)
── check: yaml ok · sha:8246dbee199f→72dd850c63b5

A YAML node that can’t be edited in place (it’s tagged) is refused rather than corrupted:

$ lets transform tags.yaml --append tags=wiki
? 7
tags.yaml is unsupported: tags cannot be changed in place (a tagged YAML node)
ERROR_CODE=unsupported_file
Back to the top of lets transform