Simple Hierarchical Config Language
A config format for files people edit by hand. Forgiving to write, predictable to read.
-
A typo on one line doesn't cause the whole config read to fail.
-
A program can change one value and leave every other line the way a person typed it, comments and all.
-
A save won't delete anything you wrote unless you tell it to.
-
Your code picks a value's type when it reads it, so
NO(e.g. for "Norway") doesn't turn intofalse, as it does with YAML. -
Rust, Go, Python, C, and C++ read a file the same way. Each library is one source file with no dependencies.
-
The CLI comes prebuilt and signed for Linux, macOS, Windows, and FreeBSD. It's all MIT licensed.
- The problem
- What SHCL does about it
- What a .shcl file looks like
- How it compares to...
- More features
- Who uses SHCL
- Escapes
- Installation
- Usage and examples
- Set up a development environment
- Docs
- Contributing and support
- Legal stuff
You have probably lived some version of this:
-
A whole service refused to start because one line of config had a typo.
-
YAML turned
country: NOintofalse. (It really does that.) -
JSON needed a comment, and JSON doesn't do comments.
- Or a trailing comma killed the parse.
-
TOML was pleasant right up until the data nested three levels deep.
-
You wanted an integer. You got a string, or an exception, or a silent zero you didn't notice until production.
-
That one project that stored a complex nested config in XML. You still have PTSD from it.
Every mainstream format makes you do the careful work, and punishes the whole file for one mistake.
SHCL (canonically pronounced "SHiCkLe") flips the burden around. Modern CPU cycles are cheap, even on embedded systems. Brainpower isn't.
So the parser does the hard work, and the person writing the file and the programmer reading it don't have to.
SHCL makes a contract:
-
Types live in your code, not in the file. The file stores text. You ask for a type when you read a value. Nothing is guessed at parse time, so there is no "Norway problem".
-
One broken line never takes down the file. It is skipped with a note. Everything else that can still be loaded safely is.
-
Every convenience read states a fallback at the call site. A missing value cannot sneak in as a silent zero.
-
If a person can tell what a line means, the parser can too.
When you do want zero-tolerance rigor: schema validation, plus a strict mode that fails loudly.
Below is an example config for a small web server - the kind of thing nginx makes you learn a bespoke brace language for. All of this is one valid file: indentation and dotted paths are interchangeable, quoting is only needed when a value contains a reserved character, and messy spacing is fine.
# Flat, TOML-style settings
listen: "0.0.0.0:443"
workers: 4
log-level: warn
# Hierarchy when you need it: one instance per site
site: example.com
root: /srv/www/example
Max-Upload-MB : 50
methods: [GET, POST, HEAD]
tls:
cert: /etc/ssl/example.pem
hsts: on
# Repeating the field adds another site - arrays of objects
# no syntax to invent
site: blog.example.com
root: /srv/www/blog
# Dotted paths give the same tree; add to any instance from anywhere
site("blog.example.com").tls.hsts: off
# Multi-line content goes in a fenced block, kept verbatim
maintenance-page:
~~~html
<h1>Down for maintenance - back in five.</h1>
~~~
Field names are case-insensitive, and repeated paths merge. Here site is a set of instances. Each of example.com and blog.example.com has its own children.
| SHCL | JSON | YAML | TOML | XML | |
|---|---|---|---|---|---|
| Comments | ✅ | 🚫 | ✅ | ✅ | ✅ |
| Unquoted strings | ✅ | 🚫 | ✅ But the parser may silently change type | 🚫 | 🚫 |
| Bad lines don't break the whole thing | ✅ | 🚫 | 🚫 | 🚫 | 🚫 |
| Who decides a value's type | Your code, at read time | The file | The parser guesses | The file | Your code |
| Deep nesting | Indent or dot paths, mixed | Brace pyramids | Indent, whitespace-fragile | [a.b.c] headers get old fast |
Tag soup |
| Nested arrays | 🚫 Use repeated instances | ✅ | ✅ | ✅ | Repeated child tags |
| Nested object definitions | ✅ | ✅ | ✅ | ✅ | ✅ |
| Multi-line verbatim blocks | Fenced, like Markdown | Escaped strings | Block scalars, with rules to memorize | Multi-line strings | CDATA |
| Hand-editable by a non-programmer | ✅ | Risky | Risky | ✅ Mostly | 🚫 |
| Tells you what it fixed | ✅ Structured diagnostics | 🚫 | 🚫 | 🚫 | 🚫 |
A note on types, because it is the big design difference: JSON and TOML store types in the file, so the author has to get them right. YAML infers types from the text, which is where
NObecomesfalse. SHCL stores plain text and coerces when you ask for a type; the only code that decides a value is an int (for example) is the code that needed an int.
Compared to other config languages has the benchmark numbers, and how SHCL differs from Pkl, CUE, and Dhall.
-
You need expressions, functions, or imports inside the file itself. Use Pkl (arguably best), CUE, or Dhall.
-
You are serializing machine-to-machine data at high volume. Use JSON or something binary. SHCL is for files that people edit.
-
You want the whole file decoded into a struct in one call, the way TOML and YAML libraries do it. SHCL reads one path at a time, and that is by design.
Beyond the list at the top:
-
The Rust reference and the Go, Python, and C/C++ bindings produce byte-for-byte identical output for valid UTF-8, and one conformance corpus tests all of them. Zig uses the C header as is.
-
Each binding is a drop-in source file. C++ adds
shcl.hppbesideshcl.h, and Go has an optional Windows file. The CLI is a single binary, and shell scripts call it directly. -
Binaries are prebuilt for Linux and Windows on x86_64 and ARM64, for macOS as one universal binary, and for FreeBSD on x86_64. macOS and FreeBSD start with 3.0.0-beta1. Everything else builds from source with
cargo install shcl. -
Releases are signed, and the binaries are reproducible from the tag.
-
There's a complete specification and a formal grammar.
-
Hierarchy comes from indentation or dot paths, such as
site(blog.example.com).tls.hsts: off, and they mix freely. Both give the same tree. -
Reads come as int, float, bool, datetime, duration, size, string, or raw block. Int, float, bool, datetime, and string reads have array forms too.
-
Repeated fields are queryable as a set. Count them, list them, or fan one read across all of them with
site(*).rootand get a status per slot. -
Raw fenced blocks work as in Markdown, and embed anything verbatim, such as SQL, code, or templates.
-
Three strictness levels, from most forgiving to fail-on-anything. At Loose,
"$1,200"reads as 1200 and15%as 0.15. Standard and Strict refuse both. -
Every diagnostic has a stable code (
E014,V001), so tooling can match on the code while the prose stays free to improve. -
Schema validation, layered loading (defaults, site, user), and commented starter-config generation, all as library features.
-
SHCL is safe on untrusted input. A parse can take caps on nodes, array size, and diagnostics, so a hostile file can't run a program out of memory. Nesting is capped at 512 levels for every parse.
-
shcl fmt --check app.shclexits 6 when the file isn't in canonical form. One line gates formatting in CI. -
A save keeps the file's mode and group, and writes through symlinks. On Windows it also keeps the ACLs and the hidden flag. It writes a temp file and renames it, so a crash can't leave half a config behind.
-
A save won't write over a change another program made after the load. It can also refuse to replace a file that's already there, and give a new file exact permission bits, such as 0600 for one with tokens in it.
-
A file that was written for a newer format version still reads, but no save overwrites it.
-
Reads and lists print JSON lines with
--json, for jq orConvertFrom-Json.shcl explain E014prints the rule behind a code, and a typo in a command, an option, or a schema field name gets a suggestion. A man page and bash and zsh completions are included. -
The C binding builds without file IO for embedded targets, and frees a whole document with one call.
-
The release gate runs the corpus, a fuzzer, and a 4-way check that feeds the same inputs to all 4 parsers and compares the bytes.
These projects use SHCL today, and harden it in the process:
| Project | OSS? | Release status | Used for |
|---|---|---|---|
| SilkTerm | Yes | beta | Config & UI language |
| GitNiblet OSS Edition | Yes | beta | Config & DDL |
| GitNiblet Enterprise Edition | No | beta | Config |
| convert-base-v2 | Yes | stable | Config |
| TradeClanker | No | beta | Config & User's rules |
Most of these are by the same author.
Escapes work differently than in most formats, and more predictably. A backslash is plain text everywhere, so a Windows path goes in as typed, quoted or not. (JSON, YAML and TOML all read the \t and \n in "C:\temp\new" as a tab and a line break, and the path quietly breaks.)
This "escape problem" is as old as computer text. It's a fix to a problem that eventually caused more problems than it solved - for something most people rarely need. It has three main causes:
-
A common keyboard character starts escape sequences, even though overwhelmingly most text doesn't need to be escaped.
-
There's no escape terminator. So either an escape is limited to one character after the backslash - and it has to be meaningful somehow - or it's hard to tell where the escape ends. (For example in C,
"\x41BC"is one escape, not an encodedAfollowed byBC.) -
There's no clear, uniform way to escape the escape character itself. (For example, matching 2 backslashes with a regex in a string literal requires
\\\\\\\\.)
SHCL avoids the whole problem. It has 3 easy ways to write a problem character, and context suggests the easiest one to use:
-
A fenced block, as in Markdown. Everything in it is passed as-is, tabs and line breaks included.
-
A value in backticks, also as in Markdown. It's never decoded, and unlike quotes,
◉is a plain character inside it. It's also a handy signal to readers that the program reading the value does the interpreting, not SHCL. -
The SHCL escape mark
◉, which begins and ends a finite list of escaped characters (including itself).
Examples:
# No surprise escapes here, it's just a literal string.
backup-dir: C:\temp\new
# The standard fenced block approach to give the calling program a formatted string without interpretation.
help-text:
~~~
The system may need one or more of:
- A reboot
- Some refreshing water
~~~
# This is returned as a literal string for the calling program to deal with, as it requested.
color: `\x1b[31m`
# Deliberate and obvious escapes, if you don't want to use a fenced block
motd: "Back soon.◉NEWLINE◉◉TAB◉Sorry for the wait."
# As with the previous example, these values here are returned by SHCL with the escapes properly interpreted.
demo-escapes:
emoji: "This is a smiley emoji: ◉U+1F600◉."
escape: "This is the SHCL escape character: ◉ESCAPE_CHAR◉"
The full list of escapes is in the spec.
Until 3.0.0 is out, the install scripts default to 2.0.0. Add --release dev (-Release dev on Windows) for the 3.0 beta that this page describes.
Linux, macOS, WSL, and FreeBSD:
bash <(curl -fsSL https://raw.githubusercontent.com/yottacore/shcl/main/install.bash) --release devWindows PowerShell:
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/yottacore/shcl/main/install.ps1))) -Release devInstallation has packages for Debian, Fedora, FreeBSD, and Windows, the install scripts for the stable and dev channels, the language packages, and a DIY route.
Usage and examples shows the shcl command from a shell, and the library from Rust, Go, Python, Zig, C, and C++. It also has Bash and PowerShell scripts, and what a save does to the file.
install-dev.bash clones the repo and installs the toolchains and linters the pipeline gates on. The one-liner and the gate to run are in Installation, and contributing.md has the full list.
-
project/install.md: packages, install scripts, the DIY route, and a development setup. -
project/examples.md: the CLI from a shell, and the library from each language. -
project/comparison.md: size and speed against JSON, YAML, TOML, and XML, and how SHCL differs from Pkl, CUE, and Dhall. -
project/spec.md: the full language spec. Terminology, types, coercion, the read API, raw blocks, strictness levels. -
project/grammar.abnf: the formal grammar. -
project/design.md: the why behind the decisions. -
changelog.md: what changed in each release. -
contributing.md: how to help. -
project/style-guide_code.md: coding and prose style. The bindings deliberately mirror the reference's structure over per-language idiom, so they stay byte-for-byte in sync. -
project/style-guide_ui-ux.md: how the command line behaves, including option spelling, the help layout, what goes to stdout and what to stderr, and the exit codes.
The generated API reference is on docs.rs for Rust and pkg.go.dev for Go.
A config parser gets trusted with other people's files, so help and scrutiny are both welcome. Bug reports, spec edge cases, and new-language bindings all count. See contributing.md to get started.
If SHCL is useful to you and code or issue reports aren't your thing, a star or a mention helps other people find it. If it saves you time, you can sponsor it through GitHub or Ko-fi.
Copyright © 2026 Jim Collier [ID: 2უNაɘ«҂թȹɤξπ๙¿ձϖ]
Licensed under the MIT License
SPDX-License-Identifier:MIT
No warranty.
SHCL™ is a trademark of Jim Collier. The name means it passes the conformance corpus.