CLI commands
This page lists the user-facing noemata commands and flags. Commands use the form noemata <command> [options]. Run noemata <command> --help for the current flag list.
Global usage
noemata <command> [options]- Running
noematawithout a command runsnoemata up. noemata --help(or-h) — list the available commands.noemata --version(or-v) — print the installed version (as the first argument only).--project <path>(or-p) — every command exceptlstakes this to target a specific project directory. It defaults to the nearest project (the closestnoemata.jsonin the current directory’s ancestry).- Only
up,initandconfigcreate a project. Every other command acts on one that already exists: with none in scope they report that and exit1, rather than offering to make one. In a repo with several registered projects and no--project, the interactive commands still offer to pick among them — choosing is a read; creating is not. - Setup writes nothing until it finishes. Press Ctrl-C at the location prompt or any wizard question to cancel. Cancellation leaves the filesystem unchanged: no
noemata.json, project directory, or entry innoemata.projects.jsonis created. The wizard creates the project after the last question. It shows a summary and does not ask for a separate final confirmation. - Cancelling a prompt exits
130(128 + SIGINT). Shells use this code for interrupted commands. A cancelled command therefore stops a chain such asnoemata up && …. Answering no to a confirmation exits0; decliningnoemata rmis a completed choice. Backing out of an offer after the work is complete also exits0, such as declining the restart prompt at the end ofnoemata config. --debug,--trace,--silent— every command takes these to raise or silence log output.
Running the stack
up
noemata up [--only <services> | --except <services>] [--detach] [--dev | --prod] [--edit] [--open | --no-open] [<configuration flags>]up regenerates the service configs and starts each managed service: ClickHouse, the OpenTelemetry collector, and the Noemata UI. It skips external services. If the project is not configured, up runs the setup wizard first (see Configuration flags). It initializes the deployment after ClickHouse is ready and before the server starts. If initialization fails, up reports the reason and does not start the server.
--only <services>— start only the listed services instead of all managed ones. Comma-separated; one or more ofdb,collector,server(e.g.--only db,collector). Other managed services are left untouched.--except <services>— the inverse: start every managed service except the listed ones (e.g.--except dbto skip ClickHouse). Same names as--only; mutually exclusive with it.--detach(-d) — spawn a detached supervisor per service instead of running attached to your terminal.--dev/--prod— force development mode (serve the UI from source with hot-reload) or production mode (the prebuilt server bundle). Mutually exclusive; auto-detected when neither is given. Development mode needs the Noemata monorepo source, so an installed CLI always runs production and rejects--dev.--edit— for this run, use the project directory as the draft of the project’sfs: remotedeployment, and turn off workflow scheduling. The server serves the files on disk layered over the published revision, so a checkout of a production config can preview edits without running production workflows.--editsetsfs.remote.diskand turns offworkflows.enabledfor the server of this run, and does not changenoemata.json. It fails for a project that is notfs: remote.--open/--no-open— open the UI in your browser once it’s ready. On by default in a terminal, off otherwise (agents/CI).--config(-c) — re-run the configuration wizard before starting.
Port conflicts. Before starting, up checks every port its managed services need. If another process holds a port, up reports the service and process, then aborts without starting services or regenerating config. When no managed services are running, the command offers to shift all managed ports to the next free +100 band and update noemata.json. If a service is already running, stop it with down before shifting ports. Without a terminal, up prints the shift it would have applied. Use down or kill to stop a stale Noemata process that holds a port.
server
noemata server [--project <path>] [--no-reconcile] [--validate] [--debug | --trace]Run the Noemata server in this process, attached, with no supervisor — one Node process where up needs two. For deployments that already supervise their processes: a container runtime, systemd, or a terminal you keep open.
It starts only the server. ClickHouse and the OpenTelemetry collector are up’s job, so server suits a deployment pointed at a ClickHouse you already operate.
- Reconciles first in a child process. By default,
serverrunsreconcilein a short-lived child process, asupdoes. The child releases memory used by frame schemas and integration data before the server starts.--validateis off by default. Use--no-reconcilewhen the project is already reconciled. - Limited configuration flags.
serverdoes not accept--no-skills,--no-external-edits,--managed, or--no-autostart. Set those options in project config.--no-integrationsis accepted; it regenerates service configs without reconciling integrations. Log-level and validation flags are forwarded to the child reconcile. - Always production.
serverruns the prebuilt server bundle; there is no development mode. Useup --devfor hot-reload from source. - No respawn. With no supervisor a crash exits the process rather than being restarted in place — which is what you want when something outside is watching. Set your orchestrator’s restart policy accordingly (
docker run --restart, systemdRestart=). A shutdown signal exits128 + signum(143 for SIGTERM), so an operator-initiated stop is never reported as a crash. - Visible to the lifecycle commands.
serverrecords the same PID files as attachedup.statusreports it, anddownstops it. The server removes its PID files when it exits and refuses to start if another live process holds them.killleaves it running; stop the process that supervises it.
down
noemata down [--project <path>]Stop every running service for the project, including supervisors started under a previous config. To hunt stray processes across all worktrees, use kill.
status
noemata status [--project <path>]Show whether each managed service is currently running.
kill
noemata kill [--all] [--project <path>]Find and stop stray Noemata processes across every project and worktree by scanning the process table — no PID files needed, so it catches daemons left behind by a deleted worktree or a backgrounded up.
--all— also stop top-levelnoemata upsessions in addition to detached supervisors.noemata serveris never stopped: it belongs to whatever is supervising it — stop that, or usedown.--project <path>— only stop processes belonging to this project (defaults to every project).
Project lifecycle
init
noemata init [--project <path>] [--restore-head <id>] [<configuration flags>]Initialize the project at --project or in the current directory so the server can serve it. Re-running init leaves an initialized project unchanged. The command does not search parent directories; running it in a project subdirectory creates a nested project.
- The project. Without a project at the target,
initruns the setup wizard and creates one (interactively it first asks where, pre-filled with the target). With one there, it keeps every setting and asks only about the settings that are unset;--yesfills those with the defaults, and a run without a terminal and without--yesasks nothing and leaves them unset. It then regenerates the managed service configs, which a fresh clone does not have. - The deployment.
initcreates the coordination table when the server config uses ClickHouse Keeper. It creates the sessions table when the server stores sessions in ClickHouse (api.session.storage: dbwith aservice_accountorhybridprincipal). It applies the file-store schema, including schema or retention updates. It then publishes revision 0, the first revision the server serves. Revision 0 contains the files the deployment served before revisions existed: files on disk forfs.local, or the store’s working files for other modes. Existing deployments stay unchanged, andinitprints the revision they serve. - A lost head. When the deployment serves no revision but its store holds revisions of generation 1 or later, the record of which revision it serves was lost, as when ClickHouse Keeper lost its data.
initthen publishes nothing, names the latest revision, and exits1;--restore-head <id>serves that revision again.
The server does not initialize a deployment. It applies no schema and publishes no revision. It refuses to start when the deployment is missing its schema, coordination or sessions table, a served revision, or a revision written by the current version. The error names noemata init as the remedy. Initializing a store requires the operator’s ClickHouse credentials. If a managed ClickHouse is not running yet, up initializes the store after ClickHouse starts. An fs: remote project also needs coordination.clickhouse_keeper in its server config. init exits 1 when an operator must resolve an initialization failure. It exits 0 when ClickHouse is not reachable yet.
init also downloads the managed ClickHouse and OpenTelemetry collector binaries into shared caches at ~/.noemata/bin and ~/.clickhouse/versions. The first run downloads a few hundred megabytes. Account for this on metered connections and in CI.
config
noemata config [--project <path>] [<configuration flags>]Run the interactive setup wizard for an existing project. It prompts for the database, collector, integrations, external config edits, agent skills, the agent panel, and HTTPS. It writes the project and managed service configs, then initializes the deployment as init does. Initialization applies any retention-policy change to the store’s schema.
reconcile
noemata reconcile [--project <path>] [--no-validate] [--validate-online] [<configuration flags>]Apply the current noemata.json to disk without starting anything: regenerate the managed service configs, reconcile the enabled integrations’ frame packs, skills, and edits to user-owned config, and align the autostart login unit. It reads the config and never writes it — config is where settings change, and it finishes by running this same reconcile. up runs it too, before starting the services.
Reach for it after hand-editing noemata.json, or to pull the current CLI version’s packs into a project without a restart.
integrations
noemata integrations add <package>[@<version>] | <path> | <id>noemata integrations remove <package> | <id>noemata integrations listnoemata integrations check [<dir>]Manage which integrations a project has, including ones published as npm packages.
addwith a package name installs it into the project directory with the project’s package manager (the one whose lockfile is present, or npm, creatingpackage.jsonwhen there is none), records it underintegrations.packagesand, by its package name, underintegrations.installed, and reconciles. A package that provides several integrations, or one that does not load yet, is recorded underpackagesonly, and the ids it provides are printed. A path installs a local checkout as a link, for developing an integration. An id of an integration the project can see, shipped or provided by an installed package, records it underinstalledwithout installing anything.removewith a package name uninstalls the package and drops its entry frompackagesand every id it provided frominstalled; with an id, it drops the entry underinstalled. Pack files stay under@frames/until a reconcile runs withremove_orphanson.listprints every enabled integration: its id, whether it ships with the CLI or which npm package and version provides it, and whether it loaded. A package that was skipped shows why: a Noemata version outside its declared range, a missing requirement, or settings that fail its schema.checkvalidates the integration package in<dir>(default: the current directory) without a project: the manifest, thecompatibilityand SDK peer ranges against the running version,requiresids that neither Noemata nor the package provides, each entry module, each skill pack, each settings schema, and the frame packs, installed into a temporary workspace beside the shipped packs theyrequireand validated asnoemata validatewould. Every problem is printed and the exit code is non-zero when there is one. See Developing locally.
Whether a package may write outside the repository is a separate decision, recorded as trusted on its entry under integrations.packages; see Trusting a package.
ls
noemata lsList every project registered in the nearest noemata.projects.json, plus the project containing the current directory, with each one’s configured state and any running services.
rm
noemata rm [--project <path>] [--force]Unlink the project from noemata.projects.json and, when confirmed, delete the files Noemata owns in the project directory (config, state, .env, and on-disk workspace content). Refuses to run while services are alive unless --force is set.
--force(-f) — stop running services and skip the confirm prompts, taking their defaults, which are yes: the project is unlinked and its Noemata-owned files are deleted, with no further confirmation. That includes@frames/,config/,.data/,.env,noemata.lock.json, and every path listed in.data/tracked.json. Run it without--forceif you want to be asked.
Authoring
run
noemata run <frame-id|path> [--sql <sql|->] [--expr <json|->] [--page <json|file|->] [--param <k=v>]... [--url <k=v>]... [--window <duration>] [--timeout <ms>] [--no-round-timerange] [--out <path>] [--json] [--project <path>]Render one frame or page against the live backend and report its runtime errors and query performance — durations, rows and bytes scanned, server time, peak memory and cache outcome, grouped under the block that issued each query. Where validate --online sweeps the whole workspace for errors, run is the tool for working on a single dashboard. It opens its own connection — see Connecting to ClickHouse.
The target is a route id or a workspace path (@frames/services/checkout.frame.json, or a path relative to your shell). A route id may be a pattern (services/{ServiceName}) or a concrete instance (services/checkout), which binds the params its path names. Static validation runs first: a statically invalid frame is reported and not rendered, because rendering one produces downstream errors that hide the real cause. Each of the view’s tables is then resolved against the backend, so a view whose columns don’t resolve is reported even though the frame would still render over it.
A run fails on any of: a static error, a runtime error, a query the backend rejected, a view table that doesn’t resolve, or a render that never settled. Rejected queries count even when the frame rendered cleanly over them — a block is free to catch a failed query and degrade, and that silent failure is exactly what this command exists to surface.
--sql <sql|->— run semantic SQL against the view: its tables and columns by name (FROM traces, view scalars as columns), compiled through the view exactly as an authored query is, so the view’s WHERE, scalars and the page’s timerange all apply. The result renders as a table and the first page is printed.--expr <json|->— the same, for a table-valued@expr/*(@expr/query,@expr/timeseries_query, …) given as inline JSON.--page <json|file|->— render this page against the target’s view instead of the page it declares, without writing anything to the workspace. It layers on top of the frame’s overlays rather than replacing them, so the view you are rendering against is the one you would see in the app. A value starting with{is inline JSON,-reads stdin, anything else is a file path. Accepts either a block ({"@block/stat": …}) or a page definition ({page, templates}). The frame’s view, params, route id and relative@block/usereferences are all unchanged — only the page differs, so a frame with no page beside it can be given one for the run.--param <column=value>— a route param, e.g.ServiceName=checkout. Required params you leave unset are sampled from live data. A concrete route id binds its own params, and those win over--param. Repeatable.--url <key=value>— URL state seeded before the render, for pinning state the page reads from the URL. Repeatable.--window <duration>— render at this window (e.g.1h,15m). Without it the route’s ownsettings.default_timerangeapplies, which is the window a user would actually see. The report says which was used.--timeout <ms>— how long to wait for the render to settle. Defaults to30000.--no-round-timerange— query the exact window. By default each query’s window expands to the bucket grid for its range length, as in the app (see_time_startand_time_end).--out <path>— where to write the JSON report, resolved against your working directory. Defaults to<project>/.local/runs/<route>-<timestamp>.json. The path is reported relative to your working directory when the file is under it, and absolute otherwise, so it can be opened as printed.--json— write the JSON report to stdout as well, and suppress everything else.--no-overrides/--no-local— skip an overlay layer. Both apply by default, so a run renders what the app renders.--page/--sql/--exprare unaffected: what you pass on the command line is applied after every overlay, so it wins outright and still applies under--no-local.
--sql, --expr and --page are mutually exclusive — each replaces the page.
ClickHouse’s query caches are disabled for every noemata run (use_query_condition_cache, which defaults to on, and use_query_cache), because the command exists to measure. Without that, a second run of the same command reads a fraction of the rows and looks faster — which is exactly the “did my change help?” comparison you would be making. Cold numbers are also the representative ones: the timerange compiles to millisecond-precision literals, so a real page view is never a cache hit either. noemata validate --online is unaffected — it reports errors rather than timings.
noemata run @opentelemetry/views/traces --sql 'SELECT ServiceName, count() AS n FROM traces GROUP BY ServiceName ORDER BY n DESC'Every run writes a JSON report (errors, per-query stats, the result preview, and the full telemetry tree) and prints its path. The per-query figures come from telemetry, so a project whose server has telemetry disabled gets a warning saying those checks did not run, rather than an all-zero report. Every run also mints a run id that is stamped on every span and log record it emits as noemata.run.id, so once the telemetry is ingested the whole run can be retrieved from the backend:
SELECT * FROM otel_traces WHERE SpanAttributes['noemata.run.id'] = '<run id>'Blocks appear in the report under their declared id when they have one, and under their positional path otherwise — so naming the blocks you care about makes the report readable. Queries served by the in-process fast path are counted in the summary but left out of the tree, where they would bury the ones that actually cost something; the JSON report keeps every query.
Exits 0 when the frame settles with no runtime errors, 1 otherwise (static failure, runtime errors, a render that never settled, or an unreachable backend).
screenshot
noemata screenshot <frame-id|path> [--page <json|file|->] [--param <k=v>]... [--url <k=v>]... [--window <duration>] [--width <px>] [--height <px>] [--scale <n>] [--timeout <ms>] [--out <path>] [--browser <path>] [--keep-browser] [--project <path>] [--dev | --prod]Render one frame in a real browser and write it to a PNG — for a pull request, an issue, or handing an agent a picture of what it just authored. The target is spelled the same way as for run: a route id — pattern or concrete instance — or a workspace path.
run cannot produce this: layout is an input to a frame’s own rendering — a plot suspends until it has a measured size — so a headless render has no chart in it.
Static validation gates the capture exactly as it does for run: a schema-invalid frame still renders, as a page of downstream failures that hide the cause, so a picture of it is a picture of noise. It costs nothing — no database connection and no browser — and runs before either is started. Runtime errors are captured rather than gated: a panel whose query failed is often the thing you are trying to show someone.
Every PNG carries a 36px Noemata banner below the frame — the mark, the wordmark and noemata.sh — so an image that travels into a pull request or a chat says where it came from.
It reuses the project’s running server when one is answering and starts a short-lived one otherwise, so it works whether or not noemata up is running. In a monorepo checkout, the short-lived server renders the UI from source; an installed CLI uses the prebuilt app bundle. Pass --dev or --prod to select a mode explicitly. --dev requires a monorepo checkout. It drives the chrome-less /render/@frames/… route — the frame with no nav bar, top bar or side panel — and captures once the frame’s render stream has fallen quiet, so panels are resolved rather than half-loaded.
--page <json|file|->— render this page against the target’s view instead of the page it declares, taking the same shapesrun --pagetakes: a bare block ({"@block/stat": …}) or a page definition ({page}). It is hosted by the frame, so it binds to the same view, params and timerange an authored page would. A value starting with{is inline JSON,-reads stdin, anything else is a file path. The CLI sends the block to Chrome over CDP instead of adding it to the render URL, so large inline blocks and files are not constrained by URL/header limits.templatesis not supported — an ad-hoc registry has no channel to the browser’s frame store, so@block/usereferences in it would not resolve; userun --pagefor that. There is no--sql/--expr.--param <column=value>— a route param, e.g.ServiceName=checkout. Substituted into the route’s dynamic segments; anything the pattern doesn’t consume becomes a query param. Repeatable. Every dynamic segment must be supplied. Unlikerun, this command opens no database connection and cannot sample a missing one, so it refuses the route up front. It has to: the app resolves an unbound{ServiceName}as a literal rather than rejecting it, which would render a frame bound to an entity that does not exist — chrome and panels intact, every one of them empty — and a screenshot that looks right and means nothing is worse than none.--url <key=value>— URL state seeded before the render, for pinning state the page reads from the URL (--url tab=latency). Repeatable.--window <duration>— render at this window (e.g.1h,15m). Without it the route’s ownsettings.default_timerangeapplies.--width <px>/--height <px>— the viewport to emulate. Defaults to1600×1000. Captures are viewport-sized rather than full-page: aheight: fillpanel has no footprint without a viewport.--heightis the height the frame lays out against; the Noemata banner adds 36px below it.--scale <n>— device pixel ratio, so the PNG stays legible when zoomed. Defaults to2.--timeout <ms>— how long navigation and settling may take together. Defaults to60000.--out <path>— where to write the PNG, resolved against your working directory. Defaults to<project>/.local/screenshots/<route>-<timestamp>.png, reported relative to your working directory when the file is under it and absolute otherwise, so it can be opened as printed.--browser <path>— the Chrome/Chromium executable to drive.NOEMATA_CHROMEsets the same thing.--keep-browser— leave the project-scoped headless browser open after a successful capture. A later screenshot reuses it. When the render URL and dev/prod mode match, a new--pagevalue replaces the block in the current document and starts a new settle cycle without refreshing the page. Pass the flag on each intermediate capture; omit it on the last capture to close the reused browser. Closing Chrome yourself is also safe; the next command discards the stale browser state.--dev/--prod— render from monorepo source or the prebuilt app bundle. When neither is given, source availability selects the mode as it does forup.
There are no --no-overrides / --no-local flags: the render resolves frames through the browser’s own store, which always applies both overlays, so a capture shows what the app shows.
noemata screenshot services/checkout --param ServiceName=checkout --window 24hnoemata screenshot services/checkout --page first.page.json --keep-browsernoemata screenshot services/checkout --page second.page.jsonThe browser it drives
The CLI ships no browser. It drives the first installed Chrome, Chromium or Edge. With none installed, an interactive run offers a one-time Chrome for Testing download into ~/.noemata/browsers; a non-interactive run (CI, a hook, an agent) fails instead of downloading ~150 MB unasked. --browser <path> or NOEMATA_CHROME skips the search.
Every capture mints a run id and prints it. The page stamps it on every span and log record it emits as noemata.run.id, and its resource carries noemata.headless = 'true', so screenshot traffic can be retrieved, or excluded from real users’ page loads. The page exports its buffered telemetry before the browser closes.
Certificate errors are ignored for the capture, so a local server’s mkcert or self-signed certificate works. The setting applies only to the headless browser managed for screenshots. The browser normally closes after the PNG is written; --keep-browser retains it under the current project’s .data/ state for reuse.
validate
noemata validate [--online] [--files <path|glob>]... [--frame <id>]... [--case <json>]... [--window <duration>] [--rollup-window <duration>] [--concurrency <n>] [--progress <spinner|lines>] [--more-progress] [--no-round-timerange] [--no-overrides] [--no-local] [--hook] [--project <path>]Validate every frame in the workspace and report any issues.
--online— render each frame against the live backend and collect runtime errors as well as static ones. It also resolves each frame’s view tables against the live schema before rendering, so a table or scalar the database can’t resolve is reported even when no panel happens to query it. See Connecting to ClickHouse for which credentials and database it uses.--files <path|glob>— validate only the frames a change to these files invalidates in the dependency graph: the files themselves plus every frame that transitively references them (editing a shared view frame or a templates file revalidates every frame that uses it, along the route chain too). Each value is a concrete file path or a glob matched against workspace paths (@frames/…) —{and}match literally, so a glob can span a parameterized frame path ('@frames/**/{ServiceName}/**'). Offline by default; add--onlineto render. Repeatable. Without it, the whole workspace is validated.--frame <id>— render only these routes, each under its default state. An id is the pattern form a route is addressed by,{Param}segments included ('@claude_code/sessions/{SessionId}/timeline'). Repeatable, and composes with--files(which routes are eligible) and--case(which states each renders under). An id matching no route fails the run rather than rendering nothing.--case <json>— a specific state combination to validate, e.g.'{"frame":"services/checkout","params":{"ServiceName":"checkout"}}'. Repeatable. A case selects its frame the way--framedoes, and a frame selected only by cases renders those cases rather than its sampled default — so--caseon one frame checks that frame alone.--window <duration>— the widest window--onlinereads (e.g.5m,1h; default15m). Online validation checks that queries run, so a short window keeps it fast. Each frame or page state keeps the last part of its own window (a--caseURL’sfrom/to, else the route’ssettings.default_timerange) up to this width. Each workflow dry run reads its most recent scheduled occurrence capped to this width (aschedule: falseworkflow reads the last--windowbefore now), and detector-generated history is bounded through the workflow run context: identity seeds (appears,disappears,value_change), burn-rate readings and component catch-up, and change-point series and component catch-up. Bucketed history uses whole buckets within the limit; a limit shorter than one bucket is rejected. Detector configuration and SLO windows remain unchanged. These dry runs check sampled data; materialized burn-rate windows without full coverage remain incomplete. This limit does not rewrite arbitrary query expressions or explicit SQL bounds. A parameterized route samples its required params from the capped window first and its full window second, and renders at the window the value came from; a route with no value in either is reported as skipped, so pin the param with--case.--rollup-window <duration>— the widest window the--onlinerollup check reads (default1h). After the frames and workflows,--onlinecompares each ready rollup installed on a table a resources file declares a rollup for with the raw table, measure by measure, at the rollup’s grain; a disagreement fails the run. See How queries read resources.--concurrency <n>— render up to N frame states in parallel during the--onlinephase (default 1). Above 1, an escaped rxjs orunhandledRejectionerror is no longer attributed to a specific frame in the report — the engine’s own error boundary still catches and attributes the common cases.--progress <spinner|lines>— how validation reports what it is doing.spinnerredraws one line naming the frame state or workflow that has been running longest, its position in the run, its elapsed time and the number of failures so far.linesprints a line per finished frame state or workflow (ok,FAILorskip, its position, and its duration), plus a line every 5 seconds for one still running; each line names its item, so the output stays readable with--concurrency. Defaults tospinneron a terminal andlinesotherwise. The summary of failures prints at the end in both modes;--hookprints no progress.--more-progress— add a start line per item and, on each finished line, the window it read (and whether it widened to find a param value), its params and its phase timings; for a workflow, its window, interval and pipeline passes. Selects--progress lines.--no-round-timerange— render--onlinestates at the exact window. By default each query’s window expands to the bucket grid for its range length, as in the app.--no-overrides/--no-local— skip an overlay layer. Both apply by default, so validation sees what the app renders;--no-localchecks the frames as a teammate or CI would (ignoring your personal.localfiles), and--no-overrideschecks them as shipped.--hook— the mode the per-edit hook (below) runs in: it reads the edited file from the hook payload on stdin, validates what that edit invalidates, and reports errors in a form the agent can act on.
--frame and --case narrow the online render; static validation still covers the whole workspace, and a static failure anywhere still gates the render. Use --files to narrow both. Rendering the workspace against live data takes minutes, so name the route you are working on:
noemata validate --online --frame '@claude_code/sessions/{SessionId}/timeline'Per-edit validation hook
When the Claude Code integration is installed, noemata up writes a PostToolUse hook that runs noemata validate --hook after every edit — re-validating just the frames the edit invalidates (the same dependency-graph resolution as --files) and feeding any errors back to the agent, so it fixes them before finishing.
The hook is written to the repo/workspace root’s .claude/settings.json — the git repo root, else the noemata.projects.json directory, else the project directory — since that’s where Claude Code resolves settings from. That way the hook loads even when the project is a subdirectory, and one root hook serves every project in the repo (the mode is resolved per edit from the edited file’s project). Because the hook is shared, it stays installed while any project in the repo requires it, and is removed once none do. It’s merged alongside your own hooks (never replacing them) and left untouched if the file isn’t valid JSON. When that root is outside the repository — no git tree, but a noemata.projects.json directory above the project — --no-external-edits skips the write.
The hook invokes the CLI that belongs to the tree it validates rather than whichever one is on PATH — a PATH install is a different build whose block schema can drift from the tree’s. When the root owns the running CLI (a source checkout, or the root’s own node_modules/noemata), the command addresses it through $CLAUDE_PROJECT_DIR, so the committed settings.json stays portable, resolves per checkout, and pins the version the project declares. A root with no CLI of its own uses the noemata on PATH, falling back to the running CLI’s own path when nothing is installed there.
Control the hook per project with integrations.installed.claude_code.hooks.validate_frames_on_edit in project config: offline (default; static-only, no backend needed), online (also renders against the live stack), or none (don’t install it).
publish
noemata publish [--project <path>] [--message <text>] [--accept-ours | --accept-theirs] [--online] [--no-validate] [--install-resources | --no-install-resources] [--dry-run]Publish what changed in the working directory as the deployment’s next revision. Each file is compared by content with the version this checkout last fetched or published, which .data/tracked.json records: an edited or added file replaces the published one, a published file deleted from disk leaves the tree, and every other published file is retained, including a file whose disk copy is older than the published one. Local files (*.local.*) and env files (.env, .env.*) never enter a revision, and the server serves neither env files nor the .data directory.
Publishing is how a change reaches the shared workflows and the other users of a deployment: they read the published revision, while the disk is a draft that only the local server serves. It connects to the deployment directly, so no server needs to be running — a checkout and, for a store, a reachable database are enough. The revision records who published it, the message, and the noemata version.
A path that changed both on disk and in the deployment since this checkout last fetched or published it is a conflict. In a terminal, publish asks for each conflict whether to keep your version, take the deployment’s version, or show the difference, and then publishes. Outside a terminal, publish refuses and lists the paths. See resolve. A revision replaces the head by compare-and-swap. When another revision is published while the plan is made, the publication is refused with a message that the deployment changed, and you can publish again.
--message <text>(-m) — the message recorded on the revision (defaults toPublish).--accept-ours— publish the disk version of every conflict.--accept-theirs— keep the deployment’s version of every conflict, publish the other changes, and then write the deployment’s version of each conflict to disk.--dry-run— list the revision changes, unpublished local files, and planned database resource steps. It does not write files or install resources.--no-validate— skip the frame validation that otherwise gates the publish. By default every frame of the planned revision, the disk over the files it retains, is validated first and nothing is published if any fails, so a schema violation can’t reach the revision, wherenoemata validate(which reads disk) would no longer see it.--online— additionally render each frame against the live backend, and refuse to publish if any fails. Rendering only runs on frames static validation accepted, so this sits inside the gate--no-validateturns off: passing both validates nothing, and says so.--install-resources/--no-install-resources— install the database resources declared by the revision, or skip installation. Installation is enabled by default. Setresources.install_on_publish: falseinnoemata.jsonto disable it. The ClickHouse user needs DDL grants; missing grants fail before installation starts. Publish again to resume an interrupted installation, even when no files changed.
resources
noemata resources <plan [--revision <id>] | prune [--dry-run] | status> [--project <path>]Inspect and clean up the database resources the deployment’s revisions declare, with the operator’s NOEMATA_CH_* credentials. noemata publish installs them.
plan [--revision <id>]— print the steps that would converge the database to the head revision, or to another revision, per physical table.prune [--dry-run]— retire undeclared rollups and__nm_*objects. Drop retired objects after seven days. Use--dry-runto review the plan.status— print the resource ledger of each database the head revision declares resources in.
Exits 0 on success (including when there was nothing to publish), 1 on a validation failure, a conflict, a deployment that changed since the plan, a deployment nobody initialized, rejected or insufficient credentials, an unreachable database, or a project with no disk to publish from.
fetch
noemata fetch [--project <path>] [--accept-theirs] [--dry-run]Write the deployment’s latest revision to the files of the checkout. fetch is the reverse of publish. Requires fs: remote and the same credentials.
Each file that still has the version this checkout last fetched or published, as recorded in .data/tracked.json, is updated to the revision’s version. Fetch writes the files the revision added and removes the files it deleted. A file you changed, added or deleted stays as it is, and fetch reports it as a local change for the next publish. A path that both you and the revision changed is a conflict.
A local server that drafts on disk (up --edit, or fs.remote.disk) makes the same updates each time a revision is published, and keeps your version of a conflicting file. Use fetch for a checkout with no server running.
--dry-run— list what would be written to disk, then exit without touching the workspace. A conflict does not block it: the paths are reported and the rest is listed as if they were left alone.--accept-theirs— take the revision’s version of every conflict: overwrite the local copy, or delete it when the revision no longer has the path.--accept-ours— keep the local version of every conflict, so the next publish publishes it.
Without either flag, fetch asks about each conflict in a terminal. Outside a terminal, a conflict stops the whole fetch.
Exits 0 on success (including when there was nothing to fetch), 1 on an unresolved conflict, a deployment nobody initialized, rejected or insufficient credentials, an unreachable database, or an fs: local project, whose deployment is on disk.
resolve
noemata resolve [<path or glob>...] [--ours | --theirs] [--project <path>]noemata resolve --discard <path or glob>... [--project <path>]Choose between the checkout’s version and the deployment’s version of each conflict. Without paths, resolve resolves every conflict. With paths, it resolves the conflicts at the matching paths. A path can be a glob pattern, such as '@frames/services/**'. Quote a glob pattern so the shell passes it unexpanded, because the shell cannot match a file you deleted locally. In a terminal without --ours or --theirs, resolve asks for each path whether to keep your version, take the deployment’s version, or show the difference. A path you skip stays as it is.
--ours— keep the local version and record the deployment’s version as its new base, so the next publish publishes your version.--theirs— take the deployment’s version: overwrite the local copy, or delete it when the deployment no longer has the path.--discard— take the deployment’s version of each matching path, whether or not the path conflicts, which discards your changes.--discardrequires at least one path and cannot be combined with--oursor--theirs.
When a file changes while its path is being resolved, the path keeps its local version and is reported. Requires the same credentials as publish and fetch.
Connecting to ClickHouse
run, validate --online, publish and fetch all open their own connection to the project’s ClickHouse — there is no server in the path. They connect as the operator: when ClickHouse requires a password (db.clickhouse.*.authentication: "password") they read NOEMATA_CH_USERNAME / NOEMATA_CH_PASSWORD from the project’s .env; when authentication is none they connect unauthenticated, as ClickHouse’s default user. They read the database and table prefix the project configures, so a rendered frame sees what the deployment serves.
They deliberately ignore api.security.principal (part of the generated server config rather than noemata.json). That setting is who the server answers requests as — typically the account that serves the deployment, which usually holds SELECT and nothing more. Publishing needs INSERT on the file store’s tables, so the two are different identities, and a deployment that exports its principal into the environment can otherwise find its publish silently attempting to write as its own read-only viewer.
For publish and fetch, both ways credentials fail are reported as such rather than as a driver error: credentials ClickHouse rejects outright, and credentials it accepts but which lack the grants for the statement. A grant is checked when the statement runs, so a SELECT-only user gets as far as reading the current state before a publish is refused — nothing is published either way.
Configuration flags
Setup asks whether to enable workflow scheduling. The default is yes, unless a
previous choice exists or the project drafts on disk against a deployment in
the store (fs.remote.disk). --yes accepts the default. This writes workflows.enabled and allows the installed
packs’ enabled rules to run. Existing site-specific opt-in rules stay disabled.
Under fs: local it then asks which workflows to run, the published ones,
your local ones, or both, and writes workflows.selection.
up, init, config, and reconcile share the flags that drive the setup wizard. They let you configure non-interactively and narrow what gets applied. (reconcile runs no wizard, so --yes and --tls don’t apply to it. server takes none of them — see its section.)
--yes(-y) — accept the default settings without prompting: a fully local stack (local ClickHouse, no authentication, files on disk, self-signed HTTPS, all detected integrations). Required to configure without a terminal; with it, the wizard runs no prompts — including the ones asking whether integrations may edit their own config outside the repository and whether to install the agent skills, so both stay on unless you pass--no-external-edits/--no-skillsor setintegrations.external_edits/skillsyourself.--tls <mode>— the TLS backend used when configuring (requires--yes):self-signed(default; encrypted HTTPS/HTTP-2 with a browser warning),mkcert(browser-trusted, but the CA install needs a terminal), ornone(plain HTTP).--no-integrations— skip reconciling integrations entirely (frame packs, skills, and edits to user-owned config like~/.claude/settings.json); regenerate only the service configs.--no-skills— reconcile everything except installing integrations’ skill packs (.claude/skills/,.agents/skills/, …). They install at the repository root, where coding agents look for them, so one set serves every project in the repo. This overrides the project’sintegrations.skillsconfig for the run — the only setting that governs them;--no-external-editsdoes not, since they never leave the repository. The wizard asks once, when they would land above the project directory, and records a decline asskills: false.--no-external-edits— keep every write inside the repository this project lives in (the project directory when it isn’t in a git tree); don’t touch anything above it. That covers integration edits to other apps’ config —~/.claude/settings.jsonand friends, which reach your machine rather than the checkout, and survive deleting the repo. It does not cover writes inside the repository (the frame packs and the agent skill packs have their own settings), the binary caches (~/.noemata/bin,~/.clickhouse/versions), the autostart login unit, ormkcert -install’s change to your system trust store; each of those has its own opt-out. This flag overrides the project’sintegrations.external_editsconfig for the run. The wizard asks the same question once — listing exactly what would be written — and records a decline in that config, so you normally set it there rather than per-run. Whatever the source of the permission, a reconcile that does write outside the repository warns and names each path.--validate/--no-validate— statically validate the frames the reconcile wrote, plus everything that references them (the same dependency-graph resolution asvalidate --files), before finishing. Needs no running backend. On by default forreconcile,init, andconfig; off forup, which reconciles on every start and shouldn’t pay for it each time. A failure exits non-zero and leaves the written files in place.--validate-online— additionally render those frames against the live backend to catch runtime errors. Off by default, and requires the stack to already be running: an unreachable database fails the run rather than skipping. Not accepted byup, whose reconcile happens before the services start — runnoemata reconcile --validate-onlineonce the stack is healthy instead.--no-autostart— leave the login unit untouched this run. By default every reconcile aligns it with the project’sautostartconfig: installing a macOS launchd agent that runsup --detachat login whenautostartis set, removing it otherwise. Independent of--no-integrations— the unit followsautostartrather than the packs.--managed/--no-managed— whether this run reinstalls the integrations’ frame packs from source. Managed (the default) keeps each pack under@frames/@<integration>/matching source, overwriting local edits;--no-managedpreserves local changes per file, and upstream still reaches everything you haven’t touched. This overrides the project’sintegrations.managedconfig for the run only — which packs are version-controlled follows the config value alone, so a one-off resync never moves files in or out of git. The wizard asks the same question, so you normally set it there rather than per-run.