Introduction

Ironclad records and compares the assumptions your software depends on.

Tests catch many failures, but they do not usually notice when:

  • a vendor changes an HTTP response
  • a CLI tool starts printing one extra warning line
  • a config file gains a field you were not expecting
  • an internal dashboard moves the sentence you scrape every Monday morning

Ironclad turns those assumptions into facts, facts into snapshots, and snapshots into material you can review directly.

The basic rhythm

Most Ironclad workflows follow the same loop:

  1. Define one or more facts.
  2. Resolve the current state into a snapshot.
  3. Compare that snapshot with the approved snapshot.
  4. Review the drift.
  5. Apply the approved changes.

The command loop is intentionally small. The main flexibility comes from the fact pipeline itself: read a file, fetch a page, split some text, extract the line that matters, and keep only that.

What Ironclad stores

An Ironclad catalog lives in .ironclad/ and usually contains:

.ironclad/
├── facts/
├── index.toml
└── snapshots/
    ├── actual.json
    └── canon.json
  • facts/ holds fact definitions.
  • index.toml maps friendly labels to fact IDs.
  • snapshots/canon.json is the approved snapshot.
  • snapshots/actual.json is the resolved snapshot.

Why this differs from plain diffing

Ironclad does not only diff files. It diffs processed observations.

That means you can compare:

  • the second JSON field inside a local file
  • the text inside a specific HTML node
  • the output of a command after trimming noise
  • a section of a file selected by tags

The result is usually more targeted than watching an entire file and diffing every incidental change.

Installation

You can install Ironclad with Cargo.

From local source

cargo install --path crates/ironclad-cli

From GitHub

cargo install --git https://github.com/truecrunchyfrog/ironclad --branch master

Verify the install

ic --help
ic op list

If ic op list prints a list of operation IDs, the CLI is installed and the built-in operations are registered.

Quickstart

This chapter shows the shortest path from an empty directory to a working fact.

1. Initialize a catalog

ic init

This creates .ironclad/ in the current directory.

2. Add a fact

ic add tea-menu

Ironclad prints either the label you chose or a fact ID if you created an unindexed fact.

3. Open the fact

ic edit tea-menu

Put a small pipeline in it:

description = "Track the teas currently advertised in the cafe window."

[[steps]]
use = "seed.file.text"
options.files = ["menu.txt"]

[[steps]]
use = "text.lines"

[[steps]]
use = "text.trim"

[[steps]]
use = "compact"

And add a file:

Jasmine Green

Smoked Earl Grey
Ube Oolong

4. Resolve the current state

ic resolve tea-menu --output -

The snapshot should contain three samples.

5. Accept it as the approved snapshot

ic apply tea-menu

If this is the first run, you can also approve everything:

ic apply --all

6. Review later changes

ic resolve
ic diff
ic inspect tea-menu
ic check

At that point you have the basic workflow: capture, compare, inspect, and approve.

Concepts

This section explains the vocabulary Ironclad uses repeatedly.

If you understand catalogs, facts, snapshots, samples, and traces, the rest of the manual becomes much easier to read.

Catalogs

A catalog is the metadata directory where Ironclad keeps facts, indexes, and snapshots.

The catalog directory is .ironclad/.

The directory above it is the container directory.

That distinction matters:

  • the catalog directory stores Ironclad data
  • the container directory is the workspace your facts usually observe

For example:

project-root/
├── .ironclad/
├── app/
├── config/
└── README.md

Here:

  • project-root/.ironclad/ is the catalog directory
  • project-root/ is the container directory

Discovery

If you do not pass --catalog-dir, Ironclad searches upward from the current working directory until it finds .ironclad/.

If you do pass --catalog-dir, it must point to the catalog directory itself. Ironclad does not append .ironclad for you.

Catalog layout

.ironclad/
├── .gitignore
├── facts/
├── index.toml
└── snapshots/
    ├── actual.json
    └── canon.json
  • facts/ holds fact files.
  • index.toml maps labels to fact IDs.
  • snapshots/actual.json stores the resolved snapshot.
  • snapshots/canon.json stores the approved snapshot.

Facts

A fact is a named assumption expressed as a small pipeline.

Examples:

  • “this config file contains exactly three upstream hosts”
  • “the lunch page still says Wednesday is dumpling day”
  • “the build tool prints the same version string as yesterday”

Each fact lives in a TOML file under .ironclad/facts/.

Labels and IDs

Facts have two identities:

  • a fact ID usually a generated ULID-like string used as the filename
  • a label a human-friendly name stored in index.toml

Many commands accept a fact selector, which can be either the label or the fact ID.

Fact shape

A fact may include:

  • description
  • imports
  • exports
  • steps
  • secret

The central part is the steps array. Each step uses an operation and optional options.

description = "Track the names of dragons currently listed in the registry."

[[steps]]
use = "seed.file.text"
options.files = ["dragons.txt"]

[[steps]]
use = "text.lines"

[[steps]]
use = "text.trim"

[[steps]]
use = "compact"

Indexed and unindexed facts

Normally you create a fact with a label, which adds it to index.toml.

You can also create one with --no-index. That leaves the fact file on disk but without a label entry in the index.

That is occasionally useful for experimentation, but indexed facts are the normal case.

Snapshots

A snapshot is a map of fact labels to batches of samples.

Ironclad usually deals with two snapshots:

  • the resolved snapshot, usually stored in actual.json
  • the approved snapshot, usually stored in canon.json

What snapshots contain

Each fact label points to a batch:

  • samples
  • created

Each sample contains:

  • content
  • traces

The content is what you actually compare. The traces explain where that content came from.

Approved and resolved snapshots

In review-oriented terms:

  • the approved snapshot is the baseline
  • the resolved snapshot is the proposal

ic diff compares the two. ic inspect lets you read one snapshot. ic apply promotes approved entries from the resolved snapshot into the approved snapshot.

What counts as drift

Drift means the batches differ.

That includes:

  • added samples
  • removed samples
  • changed content
  • multiplicity changes

If a batch used to contain one copy of a sample and now contains two, Ironclad treats that as drift.

Samples and Traces

A sample is the atomic piece of observed state in Ironclad.

Each sample has:

  • content
  • one or more traces

Content

The content is the string that operations transform and that snapshots ultimately compare.

It might be:

  • one line from a file
  • one HTML node
  • one JSON value
  • one command output

Traces

Traces are small key-value maps that explain provenance.

Examples:

{ "path": "menu.txt" }
{ "json_node_path": "$['dessert']" }
{ "start": "10", "end": "24" }

Each time an operation evolves a sample, it usually appends a new trace.

That means a single sample can tell a small story:

  1. it came from menu.txt
  2. then it was split into lines
  3. then one regex extracted a piece of it

When inspect --trace or diff --trace is useful, it is usually because that story matters as much as the final content.

Pipelines

A fact pipeline is the ordered list of steps in a fact.

Each step:

  • chooses an operation with use
  • optionally passes options
  • receives the output samples of the previous step

A tiny example

[[steps]]
use = "seed.file.text"
options.files = ["observatory.log"]

[[steps]]
use = "text.lines"

[[steps]]
use = "text.find"
options.regex = "comet-[0-9]+"

This pipeline:

  1. reads a file
  2. splits it into lines
  3. extracts comet IDs from each line

Per-sample and batch-style behavior

Most operations conceptually run per sample.

For example:

  • text.trim
  • text.replace
  • html.find
  • json.find

Some operations act more like batch filters or producers:

  • seed.* operations usually create samples from nothing
  • slice reorders or narrows the whole input batch
  • compact removes empty samples from the whole input batch

You do not usually need to think about the distinction while authoring facts, but it helps explain the shape of the output.

Secrets

Facts can be marked as secret:

secret = true

When a secret fact is resolved normally, Ironclad redacts the sample contents before writing them to snapshots.

Instead of storing the original content, it stores a digest marker.

That means:

  • you can still detect drift
  • you do not leak the secret value into the snapshot file

If you really need the unredacted values during a run, ic resolve --no-redact disables redaction for that invocation.

Use that flag carefully.

Selectors

Several commands let you choose facts by fact selector.

A fact selector can be:

  • a label
  • a fact ID

This applies to commands such as:

  • ic show
  • ic edit
  • ic rename
  • ic remove

Include and exclude sets

ic resolve works slightly differently.

It supports:

  • positional include labels
  • --exclude labels

That lets you answer questions such as:

  • “resolve only the homepage fact”
  • “resolve everything except the chatty HTTP fact”

Missing selectors

If a fact selector does not resolve to a known indexed label or an existing fact file, Ironclad fails explicitly.

That is intentional. Ignoring a misspelled selector would make command results ambiguous and harder to trust.

Guide

This guide walks through the practical use of Ironclad.

The reference chapters later in the book tell you what each command and operation does. This section focuses on how they fit together when you are building and maintaining a real catalog.

Setup

Start in the directory you want Ironclad to observe and initialize a catalog:

ic init

That creates .ironclad/ in the current directory.

.ironclad/
├── .gitignore
├── facts/
├── index.toml
└── snapshots/

The usual habit is:

  • keep .ironclad/ in the project root
  • keep the files you observe next to it in the same container directory

That keeps relative paths in fact files simple and predictable.

Using an explicit catalog

You can point commands at a specific catalog directory with --catalog-dir:

ic --catalog-dir /path/to/workspace/.ironclad inspect

Pass the .ironclad/ path itself, not its parent.

First Fact

Create a fact with a friendly label:

ic add comet-board

Open it:

ic edit comet-board

Give it a description and a pipeline:

description = "Track the currently advertised comet names."

[[steps]]
use = "seed.file.text"
options.files = ["comets.txt"]

[[steps]]
use = "text.lines"

[[steps]]
use = "text.trim"

[[steps]]
use = "compact"

Resolve it:

ic resolve comet-board --output -

Show the fact later:

ic show comet-board
ic show comet-board --path

That is enough to establish the rhythm: author, resolve, inspect.

Building Pipelines

Pipelines work best when each step does one small thing clearly.

The most reliable style is usually:

  1. seed a source
  2. split it into useful pieces
  3. normalize noisy whitespace
  4. narrow it to the parts you care about

Example: from messy bulletin board to clean samples

Imagine a hand-maintained text file:

  Neptune Parade

Moonlight Chess Club
    
Lantern Repair Night

One fact might look like:

[[steps]]
use = "seed.file.text"
options.files = ["bulletin.txt"]

[[steps]]
use = "text.lines"

[[steps]]
use = "text.trim"

[[steps]]
use = "compact"

This is more stable than diffing the whole file. You keep the meaningful lines and discard the empty ones.

Prototype with op eval

When a step is tricky, prototype it before baking it into a fact:

ic op eval text.trim --input -
ic op eval text.find --input - --options '{ text = "Neptune" }'

This is often faster than repeatedly editing the fact file.

Imports and Exports

Facts can depend on values produced by other facts.

This happens through exports and imports.

Export a sample

Exports name one sample from a fact output batch.

exports.base_url = { trace_key = "json_node_path", trace_value = "$['base_url']" }

That means:

  • find the sample whose trace contains that key/value pair
  • export it under the key base_url

Import a value

Another fact can import that key:

imports = ["base_url"]

And use it in a step option:

[[steps]]
use = "seed.net.http"
options.url = "$(base_url)"

Exact placeholder behavior

Imports are resolved only when the string value is exactly $(key).

That is important:

  • url = "$(base_url)" resolves
  • url = "prefix $(base_url)" does not

This keeps interpolation narrow and predictable.

Export key rules

Export keys must be globally unique across the resolved fact set for a single snapshot.

If two facts export the same key, resolution fails. Ironclad would rather stop loudly than guess.

Review Workflow

The review workflow is the heart of Ironclad.

Resolve

Capture the current state:

ic resolve

That writes .ironclad/snapshots/actual.json, the resolved snapshot.

Inspect

Get an overview:

ic inspect

Inspect one fact in detail:

ic inspect comet-board
ic inspect comet-board --trace

Diff

See which facts changed:

ic diff

See the detailed per-sample change records for one fact:

ic diff comet-board
ic diff comet-board --trace

Check

Use check when you want a clean success/failure exit status:

ic check

It prints either ok (0) or drift (N) and exits 0 or 1.

Apply

Approve one fact:

ic apply comet-board

Approve everything:

ic apply --all

That promotes entries from the resolved snapshot into the approved snapshot.

Working with Multiple Facts

Catalogs become interesting when they have several facts with different levels of noise, cost, and dependency.

Resolve a subset

Resolve only selected facts:

ic resolve homepage hero-sentence footer-links

Resolve everything except a few:

ic resolve --exclude slow-report --exclude weather-page

Dependencies

If facts import exported values from other facts, Ironclad sorts them so dependencies resolve first.

If a dependency cycle exists, resolution fails.

That usually means two facts are trying to derive each other’s assumptions, which is more poetic than practical.

Operating Outside a Catalog

Most commands need a catalog.

Some do not.

op eval

ic op eval can run outside a catalog when the operation does not require catalog files.

For example:

printf '[{"traces":[{}],"content":" hello "}]' \
  | ic op eval text.trim --input -

That makes op eval useful as a tiny pipeline laboratory.

When a catalog is still required

Operations that need filesystem context or catalog-backed paths still expect a meaningful working directory, and fact-related commands still require a real catalog directory.

Command Reference

This section documents every CLI command.

Each page describes:

  • purpose
  • syntax
  • arguments and options
  • behavior notes
  • examples

ic init

Initialize a catalog.

Syntax

ic init [--dir PATH]

Options

  • --dir PATH Create the catalog at the given path. If the path already ends in .ironclad, that exact directory is used. Otherwise Ironclad creates .ironclad/ inside it.

Notes

  • ic init creates the catalog directory and its initial files.
  • If the target already exists, the command fails.

Example

ic init
ic init --dir /srv/aurora/.ironclad

ic add

Create a fact.

Syntax

ic add <label>
ic add --no-index

Arguments and options

  • <label> Assign a friendly label and add the fact to index.toml.
  • --no-index Create the fact file without indexing it.

Notes

  • When indexed, the command prints the label.
  • When unindexed, the command prints the fact ID.
  • Duplicate labels are rejected.

Example

ic add tea-menu
ic add --no-index

ic edit

Open a fact in your editor.

Syntax

ic edit <selector>

Arguments

  • <selector> A fact selector: either a label or a fact ID.

Notes

  • Uses $EDITOR.
  • The command fails if $EDITOR is unset, empty, or cannot be launched.
  • The editor exit code is propagated when possible.

ic show

Show a fact summary or its file path.

Syntax

ic show <selector> [--path]

Arguments and options

  • <selector> A fact selector: either a label or a fact ID.
  • --path Print the fact file path instead of its description.

Notes

  • Without --path, the command currently prints the fact description.
  • If no description is set, it prints an empty line.

ic list

List indexed facts.

Syntax

ic list [--verbose]

Options

  • --verbose Print label: description instead of just the label.

Notes

  • Only indexed facts are listed.
  • The output order is stable and sorted by label.

ic rename

Rename an indexed fact label.

Syntax

ic rename <selector> <new-label>

Arguments

  • <selector> A fact selector: either a label or a fact ID.
  • <new-label> The new label.

Notes

  • The underlying fact file does not change; the index mapping does.
  • Reusing another label fails unless it already points to the same fact ID.

ic remove

Remove a fact file and its index entry.

Syntax

ic remove <selector>

Arguments

  • <selector> A fact selector: either a label or a fact ID.

Notes

  • The fact file is deleted.
  • If the fact was indexed, the matching label is removed from index.toml.

ic resolve

Resolve facts into a snapshot.

Syntax

ic resolve [<include> ...] [--exclude <label> ...] [--output FILE|-] [--no-redact]

Arguments and options

  • <include> ... Resolve only these labeled facts.
  • --exclude <label> ... Resolve all indexed facts except these labels.
  • --output FILE|- Write the resolved snapshot somewhere other than actual.json.
  • --no-redact Do not redact secret facts.

Notes

  • With no include or exclude arguments, all indexed facts are resolved.
  • The command prints progress to stderr while steps run.
  • Export/import dependencies are sorted automatically.

Example

ic resolve
ic resolve homepage
ic resolve --exclude noisy-banner --output -

ic inspect

Inspect one snapshot.

Syntax

ic inspect [<label>] [--trace] [--snapshot FILE|-] [--raw]

Arguments and options

  • <label> Show detailed samples for one fact.
  • --trace Include trace lines in detailed output.
  • --snapshot FILE|- Read a snapshot from a file or stdin instead of the default approved snapshot, canon.json.
  • --raw Print the entire snapshot as JSON.

Behavior

  • Without a label, inspect prints one overview line per fact: label, sample count, created timestamp.
  • With a label, it prints structured sample records.

Example

ic inspect
ic inspect tea-menu
ic inspect tea-menu --trace

ic diff

Compare two snapshots.

Syntax

ic diff [<label>] [--trace] [--proposal FILE|-] [--baseline FILE|-] [--raw]

Arguments and options

  • <label> Show detailed change records for one fact.
  • --trace Include traces in detailed output.
  • --proposal FILE|- Read the resolved snapshot from somewhere other than actual.json.
  • --baseline FILE|- Read the approved snapshot from somewhere other than canon.json.
  • --raw Print the whole diff structure as JSON.

Behavior

  • Without a label, diff prints one overview line per changed fact.
  • With a label, it prints numbered change records with explicit before and after sections.
  • Unchanged facts are omitted from the overview.

Example

ic diff
ic diff tea-menu
ic diff tea-menu --trace

ic check

Check whether two snapshots are identical.

Syntax

ic check [--proposal FILE|-] [--baseline FILE|-]

Options

  • --proposal FILE|- Read the resolved snapshot from somewhere other than actual.json.
  • --baseline FILE|- Read the approved snapshot from somewhere other than canon.json.

Behavior

  • Prints ok (0) when nothing drifted.
  • Prints drift (N) when N facts drifted.
  • Exits 0 for no drift and 1 for any drift.

This is the command you want in CI.

ic apply

Promote snapshot entries into the approved snapshot.

Syntax

ic apply <label> ...
ic apply --all

Options

  • <label> ... Promote only these facts.
  • --all Replace the approved snapshot with the full resolved snapshot.
  • --promotion FILE|- Use a snapshot other than actual.json as the resolved source.
  • --baseline FILE|- Use a snapshot other than canon.json as the approved source.
  • --output FILE|- Write the updated approved snapshot somewhere other than canon.json.

Notes

  • Applying selected labels can add, replace, or remove those labels in the approved snapshot.
  • If a requested label is absent from both the resolved snapshot and the approved snapshot, the command fails.

ic op list

List registered operation IDs.

Syntax

ic op list

Behavior

  • Prints one operation ID per line.
  • Output is sorted.

Useful when you remember the general idea of an operation but not its exact ID.

ic op show

Show operation metadata.

Syntax

ic op show <operation-id>

Behavior

The command prints:

  • the operation ID
  • the operation description
  • a TOML template of its default options, if any

Example

ic op show seed.run
ic op show text.find

ic op eval

Evaluate a single operation by hand.

Syntax

ic op eval <operation-id> [--input FILE|-] [--options TOML|-]

Options

  • --input FILE|- A JSON batch of samples. Defaults to an empty batch.
  • --options TOML|- A TOML value passed as operation options.

Notes

  • This command is excellent for prototyping pipelines.
  • It can run outside a catalog for operations that do not require catalog-backed files.
  • Avoid reading both --input and --options from stdin in the same invocation.

Fact File Reference

A fact file is TOML with a small, regular shape.

Fields

description

Optional human-readable text.

description = "Watch the currently listed moon phase."

imports

A list of export keys this fact depends on.

imports = ["base_url", "api_token"]

exports

A map of export keys to trace-match rules.

[exports.base_url]
trace_key = "json_node_path"
trace_value = "$['base_url']"

Ironclad finds the sample whose trace contains that exact key/value pair and exports it.

steps

An ordered array of operations.

[[steps]]
use = "seed.file.text"
options.files = ["status.txt"]

secret

Marks the fact as sensitive.

secret = true

Full example

description = "Track all creature names announced by the observatory."
secret = false

[[steps]]
use = "seed.file.text"
options.files = ["observatory-board.txt"]

[[steps]]
use = "text.lines"

[[steps]]
use = "text.trim"

[[steps]]
use = "compact"

Notes

  • Unknown operation options are rejected by most operations through deny_unknown_fields.
  • Import interpolation only happens for exact strings like $(key).

Snapshot Format Reference

Snapshots are JSON objects keyed by fact label.

Shape

{
  "fact-label": {
    "samples": [
      {
        "traces": [{ "path": "file.txt" }],
        "content": "hello"
      }
    ],
    "created": "..."
  }
}

Batch fields

  • samples ordered list of samples
  • created timestamp for when the batch was created

Sample fields

  • traces ordered list of trace objects
  • content string content being tracked

Stability notes

  • Snapshot files are a practical storage format, not a public network protocol.
  • They are suitable for inspect, diff, check, apply, and op eval experiments.

Operations

Operations are the verbs used inside fact steps.

Each operation page documents:

  • what the operation does
  • its options
  • whether it works per sample or over a batch
  • examples

ic op list shows the available IDs. ic op show <id> shows the description and default options template.

seed.file.text

Read text from one or more files.

Options

files = []
  • files list of glob patterns relative to the container directory

Behavior

  • Produces one sample per matched file
  • Adds a trace with path=<relative-path>
  • Fails if a file cannot be read as UTF-8 text

Example

[[steps]]
use = "seed.file.text"
options.files = ["config/*.toml"]

seed.net.http

Fetch a URL with HTTP GET.

Options

url = ""
user_agent = "Mozilla/5.0 ..."
  • url request target
  • user_agent HTTP user agent string

Behavior

  • Produces one sample containing the response body
  • Fails on HTTP error status codes

seed.run

Run one program and capture its stdout as a sample.

Options

program = ""
args = []

Behavior

  • Runs once for the whole step
  • Uses the operation working directory
  • Produces one sample from stdout
  • Fails on non-zero exit

run

Run a program once per sample, piping the sample content to stdin.

Options

program = ""
args = []

Behavior

  • Runs once per input sample
  • Writes sample content to child stdin
  • Replaces content with child stdout
  • Adds a new empty trace step to preserve lineage

Example

[[steps]]
use = "run"
options.program = "rev"

hello becomes olleh.

compact

Remove samples whose content is empty.

Options

None.

Behavior

  • Operates over the whole batch
  • Keeps sample order
  • Removes only samples whose content is exactly ""

This pairs naturally with text.trim.

slice

Take a slice of the current batch.

Options

drop = 0
take = 0
  • drop number of samples to skip from the start
  • take number of samples to keep after dropping

If take is omitted, Ironclad keeps the rest.

text.lines

Split each sample into lines.

Options

None.

Behavior

  • Runs per sample
  • Produces one sample per line

text.find

Find text matches inside each sample.

Options

text = ""
regex = ""
expand = ""

The supported options are:

  • text plain substring match
  • regex regular expression match
  • expand optional regex expansion template

Behavior

  • Produces one sample per match
  • Adds start and end trace entries

text.replace

Replace text inside each sample.

Options

text = ""
regex = ""
replacement = ""
max = 0
  • choose either text or regex
  • replacement replacement string
  • max optional maximum replacement count

text.split

Split each sample into multiple samples.

Options

The operation supports several modes:

at_index = 0
on_text.text = ""
on_text.max = 0
on_text_inclusive.text = ""

Behavior

  • at_index split at a byte index when valid
  • on_text split on a delimiter, optionally with a limit
  • on_text_inclusive keep the delimiter attached to each piece

text.tag

Extract text using Ironclad tags.

Options

tag = ""
  • tag the tag ID to select

Behavior

  • Runs per sample
  • Produces one sample per matching tag occurrence
  • Removes the tag itself from the output

See the dedicated Tags chapter for full syntax and examples.

text.trim

Trim leading and trailing whitespace from each sample.

Options

None.

Behavior

  • Runs per sample
  • Removes surrounding spaces, tabs, and newlines

This is the most common companion to text.lines.

html.find

Find HTML elements by CSS selector.

Options

selector = ""
document = false
  • selector CSS selector
  • document parse as a full HTML document instead of a fragment

Behavior

  • Produces one sample per matching node
  • Adds a node_id trace entry

html.attribute

Extract one attribute from the first element in a fragment.

Options

attribute = ""

If the attribute is missing, the result is an empty string.

html.inner.html

Extract the inner HTML of the first element in a fragment.

Options

None.

html.inner.text

Extract the inner text of the first element in a fragment.

Options

None.

json.find

Find values in JSON using a JSONPath expression.

Options

path = "$"
  • path a JSONPath expression

Behavior

  • Produces one sample per matched value
  • Adds a json_node_path trace entry
  • Strings stay strings; other JSON values are serialized back to text

Tags

Tags are Ironclad’s small embedded selection language for text.

They are meant for situations where the source text itself can carry a marker and the interesting region is relative to that marker.

This is especially handy for:

  • config files you control
  • internal notes
  • fixture files
  • documentation snippets that are easier to mark than to parse

Tag Syntax

The base syntax is:

~ic=<id>

Or with rules:

~ic=<id>=(...)

The ID is the value used by text.tag.

Minimal example

The moon is calm ~ic=moon-line

And the fact step:

[[steps]]
use = "text.tag"
options.tag = "moon-line"

Selection Boundaries

Tag rules use three boundary types:

  • line boundaries: 1L, 3L
  • byte boundaries: 4B, 20B
  • text boundaries: 'boundary text'

Lines

1L means one line boundary away.

Bytes

4B means four bytes away.

Use this carefully with Unicode text.

Text

'marker' means search for a text marker.

Escapes supported in text boundaries include:

  • \\
  • \'
  • \n
  • \r
  • \t

Left and Right Rules

Rules describe how much text to keep to the left or right of the tag.

Arrows:

  • <- select leftward
  • -> select rightward

Pipes change inclusivity:

  • |<- exclude the boundary when selecting leftward
  • ->| exclude the boundary when selecting rightward

Without a pipe, the boundary is included.

Each tag can carry up to two rules:

  • one left rule
  • one right rule

If omitted, defaults are used.

Tag Examples

Select only the tagged line

ordinary line
chosen line ~ic=pick
ordinary line

Select three lines above

line 1
line 2
line 3
current ~ic=history=(3L<-1L)
line 5

Select until a text boundary

before
START
this is the part you want
~ic=chunk=('START'|<-1L)
after

Select to the right until a marker

~ic=menu=(1L->|'dessert')
soup
salad
dessert
cake

The tag operation removes the tag text itself from the output.

Pitfalls

Bytes are not characters

B counts bytes, not graphemes. This matters for Unicode text.

Text boundary misses can be surprising

If a text boundary is absent, the selection logic falls back in a way that may not match your first guess. Test unusual rules with ic op eval text.tag.

Tags are powerful but local

Tags are great when you control the source text. They are usually the wrong tool for arbitrary external HTML or JSON, where dedicated parsers are clearer.

Configuration

Ironclad configuration comes from:

  1. CLI flags
  2. environment variables prefixed with IC_
  3. a config file

CLI flags

Global flags:

  • -v, -vv, -vvv increase log verbosity
  • --config-file PATH use a specific config file
  • --catalog-dir PATH point at the exact catalog directory

Config file

By default Ironclad looks for:

~/.config/ironclad/config.toml

Environment variables

Environment variables are read with the IC_ prefix.

Examples:

  • IC_CATALOG_DIR
  • IC_VERBOSE

Practical use

Use CLI flags when you need an explicit override.

The simplest rule is:

  • use --catalog-dir when you want one command to target a specific catalog directory
  • use environment variables or a config file for longer-lived defaults

Troubleshooting

catalog not found

Ironclad could not discover .ironclad/ from the current working directory.

Fixes:

  • cd into the right container directory
  • pass --catalog-dir /path/to/.ironclad

label not found

You asked for a fact or snapshot entry that does not exist.

Fixes:

  • run ic list
  • check spelling
  • if you meant a fact file directly, use the fact ID instead

fact selector not found

The fact selector is neither:

  • an indexed label
  • nor an existing fact ID file

Import/export failures

Typical causes:

  • an imported key was never exported
  • two facts exported the same key
  • the export trace match no longer identifies any sample

op eval with stdin

Remember that --input - consumes stdin for the batch itself. Avoid also trying to source --options from stdin in the same invocation.

Subprocess failures

Operations like seed.run and run surface:

  • non-zero exits
  • stderr
  • signal termination when possible

That usually gives you enough information to debug the underlying command.

Recipes

Clean a line-oriented text file

[[steps]]
use = "seed.file.text"
options.files = ["guest-list.txt"]

[[steps]]
use = "text.lines"

[[steps]]
use = "text.trim"

[[steps]]
use = "compact"

Scrape one HTML fragment

[[steps]]
use = "seed.net.http"
options.url = "https://example.com"

[[steps]]
use = "html.find"
options.selector = "main .headline"
options.document = true

[[steps]]
use = "html.inner.text"

Run a classic Unix filter per sample

[[steps]]
use = "text.lines"

[[steps]]
use = "run"
options.program = "rev"

Keep only interesting JSON values

[[steps]]
use = "seed.file.text"
options.files = ["status.json"]

[[steps]]
use = "json.find"
options.path = "$.checks[*].name"

Trim then remove empties

This is a common cleanup pattern:

[[steps]]
use = "text.trim"

[[steps]]
use = "compact"

Appendix

This section collects shorter reference material that is useful but not central enough for the main flow.

Glossary

  • catalog directory the .ironclad/ directory
  • container directory the directory above the catalog directory
  • fact a TOML pipeline describing one tracked assumption
  • fact selector a fact label or fact ID accepted by commands such as show, edit, and remove
  • fact ID the file-based identifier of a fact
  • label the human-friendly indexed name of a fact
  • sample one unit of tracked content
  • trace provenance metadata attached to a sample
  • batch of samples the set of samples produced by one fact
  • resolved snapshot the latest captured snapshot, usually stored in actual.json
  • approved snapshot the reviewed snapshot, usually stored in canon.json

Command Aliases

Ironclad provides a few short aliases:

  • ic rm -> ic remove
  • ic sh -> ic show
  • ic ls -> ic list
  • ic r -> ic resolve
  • ic i -> ic inspect
  • ic d -> ic diff
  • ic c -> ic check
  • ic up -> ic apply
  • ic op ls -> ic op list
  • ic op sh -> ic op show

Operation Index

Current built-in operations:

  • compact
  • html.attribute
  • html.find
  • html.inner.html
  • html.inner.text
  • json.find
  • run
  • seed.file.text
  • seed.net.http
  • seed.run
  • slice
  • text.find
  • text.lines
  • text.replace
  • text.split
  • text.tag
  • text.trim

Catalog Layout

Typical catalog tree:

.ironclad/
├── .gitignore
├── facts/
│   ├── 01...
│   └── 01...
├── index.toml
└── snapshots/
    ├── actual.json
    └── canon.json

The fact directory stores TOML files named by fact ID. The index maps labels to those IDs.