Skip to main content

flowharness (flowharness-scan console script)

flowharness drift badge

The Architectural Drift Index + Vibe-Risk scan for your repo's agent-rules surface.

Local · static · deterministic · zero network · zero model calls · zero config.


flowharness-scan reads the agent-rules surface of a repository — CLAUDE.md, .cursorrules, .cursor/rules/*.mdc, AGENTS.md, .continue/**, .opencode/**, playbook/**, and an agentharnesses bundle (HARNESS.md + skills/**/SKILL.md + routing/reference files) — and tells you two things in one pass:

  • How messy it is — the Architectural Drift Index (0–100): duplication, contradictions, orphaned rules, and cross-harness divergence.
  • How dangerous it is — a quarantine verdict: leaked secrets, indirect prompt-injection, unattested/always-on guidance.

Quickstart

No install required:

$ uvx flowharness scan .
flowharness-scan: drift index 30/100 - QUARANTINE (policy scoring/1)

uvx flowharness init scaffolds a new repo's agent-rules surface the same way, with no persistent install. flowharness and flowharness-scan are the same entry point (see below); uvx --from flowharness flowharness-scan . also works if you prefer the explicit name.

Note: these packages currently install top-level modules named core, inspection, evaluation, and portability. Prefer uvx (isolated environments) over pip install into a shared venv until the 0.2 module-namespace rename lands.

Exit codes wire straight into CI / pre-commit: PASS = 0 · NEEDS_HUMAN = 1 · FAIL = 2.

Output modes

$ flowharness-scan .                      # terminal banner (default)
$ flowharness-scan . --json               # full inspection/v1 report (content-addressed)
$ flowharness-scan . --badge > drift.svg  # local SVG badge for your README

Gating

$ flowharness-scan . --fail-on risk        # fail on the quarantine verdict (default)
$ flowharness-scan . --fail-on "index>40"  # fail when the drift index exceeds 40
$ flowharness-scan . --policy scoring.toml # custom, versioned scoring weights

Baseline (only fail on new drift)

$ flowharness-scan . --set-baseline > .flowharness-baseline.json   # commit this once
$ flowharness-scan . --baseline .flowharness-baseline.json --fail-on baseline

--set-baseline writes nothing into your repo — it prints the baseline to stdout, so the scanner stays strictly read-only on the project directory.

Platform (opt-in network: pull / upload)

The default scan path is strictly zero-network. The pull/upload subcommands and scan --upload are the only paths that touch the network (install the [remote] extra for the httpx dependency). They authenticate with a scoped bearer token.

$ export FLOWHARNESS_API_URL=https://app.example.com   # required (or pass --api-url)
$ export FLOWHARNESS_TOKEN=fht_...                      # the scoped bearer token

$ flowharness-scan pull <release_id>                    # fetch, plan and APPLY — see below
$ flowharness-scan pull <release_id> --pins-only        # fetch a release's pins -> offline cache
$ flowharness-scan . --upload --enrollment <id>         # scan, then upload the InspectionRecord
$ flowharness-scan upload sync  --enrollment <id> --release <id> --observed observed.json
$ flowharness-scan upload eval  --run run.json          # signs the report (X-Signature) + uploads

Notes: a leading token that names a real directory is scanned, not treated as a subcommand (flowharness-scan ./pull scans ./pull). A remote http:// URL is refused before any socket opens; an unset URL or token is a usage error (exit 2). Non-2xx responses map to stable exit codes (10–17); a duplicate upload (409 *write_conflict) is treated as an idempotent success, but pull GETs only and refuses every 409 (exit 13). FLOWHARNESS_TOKEN is the eval-signing HMAC key and is never written to disk.

pull is a writing verb. It used to fetch pins into .flowharness/cache/releases/ and touch nothing else; it now applies the release into the worktree. --pins-only is the old behaviour, kept — see the next section.

pull — fetch, plan and apply a release, in one shot

flowharness pull resolves one release, fetches one render plan per target harness, merges them and applies the result in a single pass. One release, one atomic decision: every plan is known before the first byte is written, so the first conflict aborts the whole pull.

$ flowharness pull [release_id]           # default: the channel head
                   [--channel <name>]     # default: [flowharness].channel, else 'stable'
                   [--harness <name>]...  # repeatable; default: config, lockfile, else detected
                   [--playbook <id>]      # disambiguate a channel name two playbooks publish
                   [--dir <path>]         # worktree (default: .); the root is discovered upward
                   [--dry-run] [--json] [--prune] [--force] [--allow-stubs]
                   [--offline]            # the saved plans only; needs an explicit release id
                   [--pins-only]          # the pre-promotion verb, kept (pins only, no writes)
                   [--api-url ...]        # else $FLOWHARNESS_API_URL

Everything above except release_id, --offline and --api-url is new in this release; --cache-root survives only as a hidden --pins-only alias (pull <id> --cache-root <path> without --pins-only is a usage error — use --dir).

$ flowharness pull
flowharness pull: rel_11 (channel stable, TLS) → claude_code, cursor · root /home/you/work/api
  update    .claude/commands/review.md
  create    .cursor/rules/testing.mdc
  applied rel_11 (was rel_9) · 2 files · fidelity: 2 native, 1 encode, 0 stub across 2 harnesses · 'flowharness pull rel_9 --prune' reverts

One line per mutating action, then the summary — a converged re-pull of a 200-file tree prints two lines, not 200. --json emits the canonical flowharness/pull/v1 payload instead, which carries every action (including unchanged/orphan) and raw, unescaped paths. The trust word is derived from the resolved URL scheme and is never signed for a merely-TLS fetch.

Exit map.

code meaning
0 applied, converged, or --dry-run (also a mutating pull whose applied.json record failed — the tree really is the release's, and that is said out loud on stderr)
2 usage (including a flag --pins-only cannot honour, and --cache-root without it), a refused flowharness.toml, no usable harness target, an ambiguous channel, an unsafe release id, or --offline with no usable saved plan
3 the apply family: a conflict, an unacknowledged behaviour-bearing stub, an unsafe path, the multi-harness surface collision apply_harness_surface_collision, or a filesystem failure — nothing written in every case except a mid-write OSError
10-17 the platform taxonomy (12 no such channel or release, and a channel that never advanced — never 0, so pull && deploy cannot read an empty channel as "nothing to do"; 13 any 409 on pull's GET routes; 15 unavailable; 16 malformed body or transport)

The config gate is fail-closed. An unreadable, malformed or mode = "serverless" flowharness.toml refuses at exit 2 before any socket opens — a verb that writes must not guess which channel it is converging onto, and an unreadable config may have been the serverless opt-out. --pins-only is exempt from the read-failure arm only, so a broken TOML cannot flip a working invocation; it still refuses a legibly opted-out repo, and it still refuses the eight pipeline flags it cannot honour (--channel, --harness, --playbook, --dry-run, --json, --prune, --force, --allow-stubs) at exit 2 rather than accepting and ignoring them. Its stdout, exit code and single request are otherwise byte-for-byte the pre-promotion verb's.

Saved plans. Every fetched plan lands in .flowharness/cache/plans/<release>.<harness>.json in a marked, clock-free flowharness/plan-cache.v1 envelope, so --offline and status work without the network. The directory keeps the current and previous release per harness, and eviction runs on every terminal outcome of a run that resolved a release it did not already have — applied, --dry-run, and a conflict abort alike. Two outcomes evict nothing: a refusal before the release is resolved (it cached nothing to evict), and a converged re-pull of the release the lockfile already records — that run superseded nothing, and evicting would take out the previous release's plans, which is the pull <older id> the last summary offered as the undo. An eviction that fails on I/O is swallowed rather than failing the pull. --offline reads that cache — not the pin cache, which only --pins-only --offline reads — and opens no socket at all.

Reversibility is git plus the lockfile — no backup files, no rollback verb. pull <older id> is the undo, and the summary prints the exact command after a pull that changed the release (with --prune when this release rendered paths the previous record does not track, since without it the printed command would leave the extras behind). The sidecar's cache/ and personal/ are git-ignored for you on the first mutating pull — and on a --dry-run, which creates the caches.

status — am I running what the channel publishes?

flowharness status answers two questions in one line — is the applied release the channel's head? and has anyone edited the governed files? — and it is strictly read-only: it writes nothing in any mode, and under --offline it opens no socket and imports no httpx at all.

$ flowharness status
flowharness: 2 releases behind stable (rel_9 → rel_11) — 'flowharness pull' to catch up

$ flowharness status --json          # the flowharness/status/v1 payload, canonical bytes
$ flowharness status --check         # exit 7 on proven divergence; stdout is unchanged
$ flowharness status --offline       # local facts only; freshness is reported as unknown
$ flowharness status --dir path/to/repo --channel beta --api-url https://app.example.com

flowharness and flowharness-scan are two names for the same entry point — the examples above and below use them interchangeably, and every subcommand works under either.

Run it from anywhere inside the repo: status walks up to the nearest ancestor holding .flowharness/ or flowharness.toml, stopping at a .git boundary, the way git status does. --channel overrides [flowharness].channel, which overrides the stable default. --api-url is accepted and ignored under --offline.

Exit map. Bare status always exits 0 — a corrupt lockfile, a dead network, a typo'd flowharness.toml, or an unreadable governed file is a reported state, never an exit code, so the verb is safe in a shell prompt. --check returns 7 on proven divergence (a governed file drifted, or the applied release is provably behind the head); if nothing diverged but the probe itself failed, it returns that failure's code (10–17). A bad flag or a --dir that does not exist is the usual argparse 2.

Divergence requires proof: an unreadable origin, an unrecognised origin, or an unorderable pair of release versions is reported as unknown (drift.known: false in the JSON) and --check returns 0 rather than claiming a divergence it did not observe.

Not yet here. --diff (show the actual edits) lands with PR-D; serverless freshness (a git remote instead of a channel) lands with PR-F.

Known limitations. Drift is byte-comparison against the lockfile's hashes, so: a filename stored NFC on Linux and NFD on macOS can flip between "converged" and "missing", and core.autocrlf=true on Windows makes every governed text file read as modified. Every governed file is re-hashed on every run (a megabyte at a time — a huge file is slow, never an out-of-memory), and a console that cannot encode the banner's ·// gets those characters back-slash-escaped rather than a traceback.

Behind a corporate proxy or a private CA, note that the client sets trust_env=False: status, pull and upload deliberately ignore HTTP_PROXY/HTTPS_PROXY and SSL_CERT_FILE/SSL_CERT_DIR/REQUESTS_CA_BUNDLE, and there is not yet an explicit option to put them back.

Pre-commit

# .pre-commit-config.yaml
-   repo: https://github.com/suleimanmahmoud/flowharness
    rev: <tag>
    hooks:
    -   id: flowharness-scan

What it flags

Dimension Check Catches
duplication DuplicationCheck the same guideline pasted across files/sections
contradiction ContradictionCheck retry-vs-fail-fast, tests-first-vs-prototype, …
orphans OrphanCheck rules no active harness loads; leftovers of removed tools
divergence DivergenceCheck the "same" rule that differs across harnesses
risk LeakageCheck secrets/credentials/template-vars — redacted to fingerprints
risk InjectionHeuristicsCheck indirect prompt-injection (override / exfil / fetch-and-execute)
risk AttestationCheck / BoundsCheck unattested or implicitly always-on guidance

A finding never contains a secret value; the report carries structural metadata only (no rule-body text).

Guarantees

  • Deterministic — identical directory state ⇒ byte-identical report; ids and the report_hash are content hashes; no timestamps.
  • Read-only & offline — the scanner only ever reads the project directory; no network, no model calls on the default path.
  • Fast — a 50-unit repo scans in tens of milliseconds (budget: < 2 s).

Report

--json emits the inspection/v1 schema — snapshot metadata, redacted findings, the risk assessment, and the scored drift index — documented in docs/specs/inspection-report-v1.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flowharness-0.1.1.tar.gz (181.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flowharness-0.1.1-py3-none-any.whl (218.8 kB view details)

Uploaded Python 3

File details

Details for the file flowharness-0.1.1.tar.gz.

File metadata

  • Download URL: flowharness-0.1.1.tar.gz
  • Upload date:
  • Size: 181.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for flowharness-0.1.1.tar.gz
Algorithm Hash digest
SHA256 c2679ada0b36f8ee24ae9e5ecb33cce8492e98534fe818cc6c0571ea894e3e7f
MD5 fa1c0b564bc4e94e401eedb4c360a0a5
BLAKE2b-256 d4a5cbc1086209eecc8e405b6ebd252ceff12b3258f3ddbbc112073f20766710

See more details on using hashes here.

Provenance

The following attestation bundles were made for flowharness-0.1.1.tar.gz:

Publisher: release.yml on suleimanmahmoud/flowharness

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flowharness-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: flowharness-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 218.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for flowharness-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 1dd0310ec9620d23363bebd48aa5475d71cbd3ee33f11bd023cd319930b61e4c
MD5 58e9bac44d49bb1ae3cc71898e0694fb
BLAKE2b-256 b9f92dc19eeb29d7c063be71fd1b5b975b1611b5d8df7fb13af368e63339a2cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for flowharness-0.1.1-py3-none-any.whl:

Publisher: release.yml on suleimanmahmoud/flowharness

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

2 files

0.1.2

2 files

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page