Edit config files without reflowing them.
You want to change one value in a commented, hand-formatted config. Today every option costs you something:
$ jq '.compilerOptions.target = "ES2022"' tsconfig.json
# comments gone, trailing commas gone, the whole file reindented,
# and JSONC-with-comments won't round-trip through jq at alledikt changes the one value and nothing else:
$ edikt -i '.compilerOptions.target = "ES2022"' tsconfig.json
# one line of the diff moves; every comment, blank line, indent, and
# trailing comma comes back byte for byteedit, meets edict. A lossless, format-preserving editor for JSONC/JSON5,
TOML, YAML, INI, KDL, and flat key-value files (.env,
.properties), driven by a jq-flavored expression language and a sed-shaped
execution model. It touches only the bytes you point at.
# query - reads like jq
edikt '.compilerOptions.strict' tsconfig.json
# edit in place - comments, indent, comma style all preserved
edikt -i '.compilerOptions.target = "ES2022"' tsconfig.json
# edit YAML in place - anchors, flow style, and comments survive
edikt -i '.services.web.replicas = 3' compose.yaml
# compute, not just place
edikt -i '.version |= . + "-dev"' package.jsonc
# stream-first, like sed (stdin has no extension, so name the format)
cat settings.jsonc | edikt -t jsonc 'del(.telemetry) | .theme = "dark"'
# script from a file
edikt -f release.edk -i config.jsonc
# convert, where feasible - comments carried across, in the target's syntax
edikt -T yaml tsconfig.jsonc
# Markdown frontmatter - edit the metadata block, the prose untouched
edikt -i '.draft = false' post.md
# multi-document YAML - one edit maps over every ---document in the stream
edikt -i '.metadata.labels.env = "prod"' k8s.yaml
# ...or target one document by position with ^dN (0-based, strict)
edikt -i '^d1 | .spec.replicas = 3' k8s.yaml
# PEP 723 / scriptbox blocks - bump the pin, the code below untouched
edikt -t frontmatter -i '.["requires-python"] = ">=3.12"' app.pyEvery one of these writes back a file that is byte-identical except for the value you changed, the kind of diff a reviewer reads at a glance.
Markdown frontmatter. For a .md/.markdown/.mdx/.qmd file (or
-t markdown), edikt edits the leading metadata block, YAML (---), TOML
(+++), tagged (---json), or Hugo bare-brace JSON, and leaves the document
body byte-for-byte opaque. edikt '.title' post.md queries it;
edikt -i '.draft = false' post.md rewrites one key with the prose intact.
Commented host-language blocks. edikt reads PEP 723 # /// script ... # ///
blocks in a Python file (uv) or a shell script (scriptbox), bumps the pin, and
re-applies the # prefix with the code below untouched.
Multi-document YAML. A ----separated stream (Kubernetes manifests, Ansible,
Helm output) is first-class. An edit maps over every document by default and
silently skips any that lack the target path; a query returns one result per
document. select(pred) targets documents by content
(select(.kind == "Service") | .spec.type = "LoadBalancer"), and ^dN selects
one by position (^d0 is the first), strict there, so ^d5 on a
three-document stream is an error, not a no-op.
brew install jhheider/tap/edikt # Homebrew
cargo install edikt # Cargo (crates.io)
pkgx install edikt # pkgx (builds from source)Or grab a prebuilt binary (Linux, macOS, and Windows; x86_64 and arm64) from the releases page.
edikt is happy editing the config it ships in. Here is the whole version bump
for a release, a Cargo.toml per crate plus the internal dependency pins in
the workspace root, done losslessly:
# each crate's own version
for c in crates/*/Cargo.toml; do
edikt -i '.package.version = "1.2.3"' "$c"
done
# the [workspace.dependencies] pins (inline tables), chained with -e
edikt -i \
-e '.workspace.dependencies."edikt-core".version = "1.2.3"' \
-e '.workspace.dependencies."edikt-syntax".version = "1.2.3"' \
Cargo.tomlEach edit moves exactly one version = "...". The # Internal crates comment
above the dependency table, the path = "crates/edikt-core" sitting next to
each version, the brace-and-space style of every inline table, and every other
byte come back unchanged, the kind of diff a reviewer reads at a glance.
One quoting note: a key containing - is written ."edikt-core" (or
.["edikt-core"]), because a bare .edikt-core would read the - as
subtraction. edikt says so with a clear error rather than guessing.
Honestly, a weekend project, one that started with a real itch. The problem crystallized while reading "Respectful" YAML Patching in Rust, which surveys the Rust libraries for patching YAML and lands on the gap in one line: none of them preserve both the formatting and the comments. That's the exact thing I kept wanting, surgically change one value (or one comment) and leave every other byte, comment, and blank line alone, and not just for YAML but for the whole pile of config formats a project accumulates.
The good tools each own their corner: jq and yq for querying, taplo and
prettier for formatting, and the excellent toml_edit and kdl-rs crates for
lossless edits (edikt is built on those last two). edikt isn't trying to
replace them, it's the piece I couldn't find off the shelf: one jq-flavored
tool that edits and queries and converts across JSONC, INI, TOML, YAML,
KDL, and .env, touching only the bytes you point at. If your need is
single-format, reach for the specialist; if it's "the same surgical edit, across
all of these," that's the gap this fills.
It edits values and comments; it does not reformat, lint, or validate schemas,
reach for taplo/prettier for that. The expression language is a curated
subset of jq (navigation, mutation, arithmetic, // defaults, regex,
split/join); if/then, reduce, variables (as $x), and user-defined
functions are not in v1. And .env is flat and string-valued, always. Stating
the boundaries plainly is the point: inside them, the surgical-edit promise
holds byte for byte.
Seven config formats with lossless in-place edit, query, and comment-preserving
conversion, plus a frontmatter lens (Markdown and PEP 723 host-language blocks)
and multi-document YAML streams with select/^dN targeting. On crates.io,
Homebrew, and pkgx, the badge above tracks the current version. See
CLAUDE.md for the build contract.
- Exit codes are sed-shaped:
0= success, a query that matches nothing is a silent no-op (safe underset -e);2= parse or evaluation error. For presence tests,--exit-statusopts into jq's1on zero matches; for defaults, use//:edikt '.maybe.key // "fallback"' f.yaml. - The expression language is deliberately capped in v1. jq's navigation,
mutation, arithmetic,
//defaults, and a curated builtin registry (including regextest/match/capture/sub/gsub,split/join) are in; variables (as $x),if/then,reduce, and user-defined functions are not (yet). - Many files, sed-style:
edikt -i '.v = 9' a.json b.json(let the shell glob:edikt -i 'del(.telemetry)' config/*.jsonc). Queries over several files concatenate results in order. .envis flat and string-valued, no arrays or nesting, ever, but string computation on values works fine:edikt -i '.VERSION |= sub("^v"; "")' .env.
Licensed under either of Apache License, Version 2.0 or MIT license, at your option.