Know what your config actually changed — and whether it's risky.
Config drives the parts of a system that break loudest: connection pools, feature
flags, TLS, retries, secrets. But we still review it as text — so a reordered key
looks identical to a doubled pool size, and debug: true slips through in a
40-line formatting diff.
Flecto reads config as structure, not lines. It tells you what changed in plain English, flags what looks risky, and gives you an exit code to gate on.
| Reviewing config without Flecto | With Flecto |
|---|---|
+ 40 lines of YAML noise |
~ database.pool_size: 5 → 20 |
Hope someone notices debug: true |
Policy finding → build fails |
"Something in .env changed" |
The exact keys, with secrets masked |
The same engine reads whatever your change actually lives in:
| You are reviewing | Flecto reads |
|---|---|
| App config — YAML, JSON, TOML, INI, dotenv | the files directly |
| A Terraform change | terraform show -json output, via flecto plan |
| A Kubernetes change | rendered manifests from helm, kustomize, or anything else |
| A SOPS-encrypted file | its structure and recipients — never its plaintext |
It never invokes terraform, helm, kustomize, sops, or age, so nothing
extra has to exist on the CI runner.
npm install -g flectoRequires Node.js 20.19.0+. Verify:
flecto --version
flecto doctorPrefer not to install globally? Every example below works with
npx --yes flecto@3 instead of flecto.
A complete walkthrough, start to finish. Copy-paste it anywhere.
1. Create a config file to track.
mkdir flecto-demo && cd flecto-demo && mkdir config
cat > config/prod.yaml <<'EOF'
database:
host: db.internal
pool_size: 5
ssl: true
logging:
level: info
debug: false
EOF2. Save it as your baseline.
flecto watch config/prod.yaml --snapshot✓ Snapshot saved: /path/to/flecto-demo/.flecto-snapshots/4b8cbbd70d1832a2.json
3. Make the kind of edit that causes incidents.
cat > config/prod.yaml <<'EOF'
database:
host: db.internal
pool_size: 20
ssl: true
logging:
level: info
debug: true
EOF4. Ask what changed.
flecto watch config/prod.yaml --diff/path/to/flecto-demo/config/prod.yaml — 2 changes from snapshot:
~ database.pool_size: 5 → 20
~ logging.debug: false → true
Two sentences instead of a diff you have to interpret. Now let Flecto judge it:
flecto ci config/prod.yaml --format github-annotations::warning title=flecto changed::database.pool_size
::warning title=flecto changed::logging.debug
::warning title=flecto policy pool-size-jump [default]::database.pool_size: Pool size increased from 5 to 20 (>=2x).
::error title=flecto policy dangerous-toggle-enabled [default]::logging.debug: Potentially dangerous toggle enabled.
Exit code 1. In CI, that's a failed build — before the change ships.
5. Watch it live. Leave this running and edit the file in another window:
flecto watch config/prod.yamlflecto watching /path/to/flecto-demo/config/prod.yaml
Press Ctrl+C to stop.
[18:24:48] /path/to/flecto-demo/config/prod.yaml — 2 changes
~ database.pool_size: 5 → 20
~ logging.debug: false → true
! policy(warn) [default] database.pool_size: Pool size increased from 5 to 20 (>=2x).
! policy(error) [default] logging.debug: Potentially dangerous toggle enabled.
That's the whole product. Everything below is depth.
Add one step to your workflow and risky config edits show up as annotations on the pull request:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 2
- uses: myselfsiddharth/Flecto/.github/actions/flecto-ci@main
with:
targets: config/**/*.{yaml,yml,json,toml,ini}
snapshot-ref: HEAD~1Prefer a summary nobody can miss? --format pr-comment renders the changes and
policy findings as markdown and, when you opt in with --pr-comment-post inside
a GitHub PR run, keeps one sticky comment up to date instead of adding a new
one per push. Without that flag it just prints the markdown, so it can't post
from your laptop.
The flecto-pr-risk Action is that, packaged — the whole adoption is one
uses:, with the baseline resolved from the pull request rather than HEAD~1:
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: myselfsiddharth/Flecto/.github/actions/flecto-pr-risk@mainA fork's pull request gets a read-only token, so the comment is skipped with a warning there — the check itself still runs and still fails on risky changes.
Works on any CI runner — it's a plain CLI with meaningful exit codes. → CI guide
Restart a service, reload a process, or notify an endpoint whenever config moves:
flecto watch .env --command "docker-compose restart app"
flecto watch config/prod.yaml \
--webhook https://hooks.example.com/notify \
--delivery-mode at-least-onceChanges arrive as a versioned JSON envelope, and at-least-once persists and
retries failed deliveries.
Posting straight to chat needs no receiver of your own — --webhook-format
shapes the body for Slack, Discord, or Teams, colored by the highest policy
severity:
flecto watch config/prod.yaml \
--webhook "https://hooks.slack.com/services/T000/B000/XXXX" \
--webhook-format slack"Works in staging, fails in prod" is usually one key apart:
flecto compare config/prod.yaml config/staging.yaml"+" exists only in the compared file, "-" only in the baseline, "~" differs
/path/to/config/staging.yaml — 2 changes from /path/to/config/prod.yaml:
- only_in_prod: true
~ database.pool_size: 5 → 20
! policy(warn) [default] database.pool_size: Pool size increased from 5 to 20 (>=2x).
The first file is the baseline, the files don't have to share a format, and
--format json gives you the same output flecto ci produces.
→ CLI reference
flecto history config/prod.yaml --limit 10Snapshots stay on your machine in .flecto-snapshots/. Nothing is uploaded and
no account is required. → CLI reference
flecto report --limit 20 --mask-secrets --output drift.htmlOne HTML file from that same local history: a timeline per file, every change with its UTC timestamp, and policy findings grouped by severity. Fully self-contained — inline styles, no fonts, no CDN scripts, no analytics — so you can attach it to an incident thread and it renders offline. No server and no account, same as everything else here. → CLI reference
ArgoCD, Flux, and helm diff compare the cluster to the repo — which needs a
cluster, and an apply that already happened. Flecto compares the manifests this
pull request would produce against the ones main produces:
helm template api ./charts/api -f values/prod.yaml > /tmp/head.yaml
flecto compare /tmp/base.yaml /tmp/head.yaml --policies kubernetes --fail-on error~ Service/prod/api.spec.type: "ClusterIP" → "LoadBalancer"
! policy(error) [kubernetes] Service type is LoadBalancer, which exposes the
workload outside the cluster. Confirm the exposure is intended.
Multi-document YAML is keyed by kind/namespace/name, so findings name the
resource. Flecto never runs helm or kustomize — you render, it diffs, so any
renderer works and no binary is needed in CI.
→ Kubernetes
terraform plan output is precise and long. Flecto turns it into the handful of
lines a reviewer actually needs to argue about:
terraform show -json plan.tfplan > plan.json
flecto plan plan.json --fail-on errorplan.json — plan format 1.2
Plan: 0 to add, 1 to change, 0 to destroy, 1 to replace.
~ aws_security_group.web.ingress[0].cidr_blocks[0]: "10.0.0.0/8" → "0.0.0.0/0"
- aws_db_instance.main.#action: "replace" [terraform will destroy and recreate aws_db_instance.main]
~ aws_db_instance.main.password: "(sensitive value)" → "(sensitive value)" [sensitive]
! policy(error) [terraform] …cidr_blocks[0]: Security group ingress will accept
traffic from the whole internet (0.0.0.0/0). Restrict the source to a known CIDR…
! policy(error) [terraform] …#action: Terraform will destroy a stateful resource.
Its data does not survive. Take a final snapshot, or add a prevent_destroy…
A replace reads as a removal, not a benign update — a recreated database
should never look like a config tweak. Values Terraform marks sensitive are
redacted during parsing, before the policy engine or any formatter sees them, and
after_unknown renders as (known after apply) rather than null.
Flecto never runs terraform — you produce the JSON, it reads it, so nothing
extra has to exist on the CI runner.
→ Terraform plans
Beyond the built-in packs, write rules as declarative JSON or YAML — no code:
{
"id": "risky-feature-enable",
"severity": "error",
"allOf": [
{ "match": { "pathPrefix": "features." } },
{ "afterTruthy": true }
]
}For anything a predicate can't express, a local ESM plugin exporting
evaluate(changes, ctx) gets the full change set.
→ Writing policy packs · Plugins
flecto watch config/prod.yaml --ignore "updated_at,**.meta.timestamp"Arrays of objects are matched by id or name, so reordering a list of named
services doesn't read as a wall of changes.
→ Configuration
| Pack | Catches |
|---|---|
default |
Secret-like keys added or changed, secret-shaped values under any key, dangerous toggles, pool-size jumps |
strict-prod |
The same ground, with production-grade severities and matching |
compose |
Privileged services, host networking, Docker socket mounts, sensitive bind mounts |
kubernetes |
Privileged containers, host namespaces, weakened runAsNonRoot, added SYS_ADMIN, unpinned images, replica jumps, dropped limits, LoadBalancer/NodePort exposure |
node-runtime |
Dropped engine requirements, TLS verification bypasses, debug/inspector flags |
terraform |
Replaced and destroyed stateful resources, ingress opened to 0.0.0.0/0, IAM wildcards, public S3, capacity jumps |
sops |
Decryption recipients added or removed, a MAC that moved on its own, a file that stopped being encrypted |
flecto policies list # see what resolves here, built-in and local
flecto ci "config/**/*.yaml" --policies "default,strict-prod" --fail-on policyPass files or glob patterns, quoted so your shell doesn't expand them first — a bare directory is not a valid target.
A local policies/<id>.json overrides the built-in pack of the same id, and
severityRemap raises or silences individual rules per profile without forking
anything. → Policy packs
Community packs ship on npm as flecto-pack-<id> packages — a package name and
one declarative JSON file, nothing else:
npm install --save-dev flecto-pack-deployment-safety
flecto policies add deployment-safetypolicies add validates the pack, writes it to policies/deployment-safety.json,
and runs no code from the package. →
Installing community packs
| Format | Extensions |
|---|---|
| JSON | .json |
| YAML | .yaml, .yml |
| TOML | .toml |
| INI | .ini |
| dotenv | .env, .env.*, *.env |
| age (armored) | .age, or any file whose contents are one armored blob |
Terraform plan JSON (terraform show -json) is read by flecto plan, which
applies Terraform's own sensitivity marking. Point plan at it rather than ci
or watch — those treat it as ordinary JSON and will print values Terraform
marks sensitive (#113).
Multi-document YAML (----separated, the usual shape of a Kubernetes manifest)
is supported. Each document is diffed under its own key — kind/name for
Kubernetes-shaped documents, so a document inserted at the top of the file does
not renumber every other path. →
Multi-document YAML
A sops- or age-encrypted file is detected from its contents, and diffed
structurally:
+ cache: {"ttl_seconds":300}
~ database.password: <encrypted value changed>
+ sops.age.age1exampleexample…: {"recipient":"age1exampleexample…","enc":"<encrypted value>"}
You get keys added and removed, which encrypted values moved, and — the useful
part — who can decrypt the file. A recipient added is a genuine security event
and the sops pack raises it as one.
Flecto never decrypts. It never shells out to sops or age, never reads a
key file or agent socket, and never prints ciphertext — not even without
--mask-secrets. Ciphertext is replaced with an opaque sentinel in the parser,
so no diff, snapshot, webhook, or report can carry it. →
Encrypted files
Most teams commit a .flectorc so local runs and CI agree:
flecto init{
"defaults": {
"policies": ["default"],
"ignore": ["**.updated_at"]
},
"profiles": {
"dev": { "mode": "verbose" },
"ci": { "failOn": "policy,error" },
"prod": {
"policies": ["default", "strict-prod"],
"severityRemap": { "pool-size-jump": "error" },
"maskSecrets": true
}
},
"files": ["config/**/*.{yaml,yml,json,toml,ini}", ".env"]
}flecto watch --profile dev
flecto ci --profile ciExplicit CLI flags win over profiles, which win over defaults.
→ Full configuration reference
| Command | What it does |
|---|---|
flecto watch [files...] |
Watch for changes and print them as they happen |
flecto watch --snapshot |
Save the current state as a baseline |
flecto watch --diff |
Compare against the baseline and exit |
flecto ci [files...] |
One-shot check with a gate-able exit code |
flecto compare <fileA> <fileB> |
Diff two files against each other (fileA is the baseline) |
flecto plan <planFiles...> |
Review terraform show -json output and gate on it |
flecto history [files...] |
Summarize drift across local snapshots |
flecto report [files...] |
Render that history as a self-contained HTML file |
flecto policies add <name> |
Install a pack from an flecto-pack-* npm package |
flecto policies list |
List available policy packs |
flecto policies test <dir> |
Assert pack and plugin findings from fixtures |
flecto init |
Create a .flectorc from detected stack signals |
flecto doctor |
Check setup, config, and environment |
| Guide | Covers |
|---|---|
| CLI reference | Every command, flag, and exit code |
| Configuration | .flectorc, profiles, ignore patterns, array identity, masking |
| Encrypted files | SOPS and age: what is detected, what is reported, why nothing is decrypted |
| CI | Baselines, fail triggers, output formats, the bundled GitHub Actions |
| Kubernetes | Diffing rendered Helm/Kustomize manifests before they reach a cluster |
| Terraform plans | Reviewing terraform show -json output and the terraform pack |
| Webhooks and commands | Envelope shape, delivery modes, command environment |
| Policy packs | Writing declarative rules |
| Plugins · Cookbook | Rules that need real code |
| Troubleshooting | When something doesn't behave |
| Changelog | Release history and migration notes |
- Parse — format detected by extension or dotenv naming → structured values
- Watch — chokidar with debounce
- Diff — semantic tree comparison with ignore rules and array identity
- Evaluate — policy packs and plugins → severity-tagged findings
- Emit — a versioned envelope (
schema_version: "2.0") - Deliver — terminal output, shell command, webhook, or CI annotations
Flecto runs entirely on your machine. Snapshots are local files, and nothing leaves the process unless you configure a webhook or command.
- Questions and ideas — Discussions
- Bugs and requests — Issues
- Contributing — CONTRIBUTING.md · Code of Conduct
- Security — SECURITY.md, private disclosure only
- Roadmap — Milestones
Released under the MIT License.
