nitctl
The operator’s tool: it validates policy bundles, explains decisions, issues tokens, applies migrations, and inspects the queue and the audit trail.
nitctl config show|path|initnitctl policy validate|explain|show <bundle-dir>nitctl migrate -dsn <postgres-dsn> [-status]nitctl token create|list|revokenitctl statsnitctl tasksnitctl auditTwo families, with different needs:
stats,tasks,auditgo through the API and needNIT_SERVERand an adminNIT_TOKEN. They are deliberately the same endpoints the web console calls, so the API is exercised from day one and the UI can never need a capabilitynitctllacks.config,policy,migrate,tokenare run on the server and read the configuration file and the database directly.
nitctl config
Section titled “nitctl config”nitctl config path # which file would be readnitctl config show # every effective value, and where it came fromnitctl config init # write a commented starter file, mode 600nitctl config init -config ./nit.yaml -force| Flag | ||
|---|---|---|
-config | /etc/nit/nit.yaml for init | Which file |
-force | Overwrite an existing file (init only) |
file: /etc/nit/nit.yaml
SETTING FROM VALUEaddr file 127.0.0.1:8080database.url file postgres://postgres:***@localhost:5432/nitlog.level env DEBUGpolicy.reload default 30ssecurity.sync_key file (set)The FROM column answers why is this setting what it is? — the question an
operator asks at the worst possible moment, whose honest answer otherwise needs
three places checked.
Secrets are never printed: only whether they are set and which layer supplied them. A database URL keeps its host and user and loses its password.
nitctl policy
Section titled “nitctl policy”validate
Section titled “validate”nitctl policy validate ./policybundle ok version: sha256:f6040b6d6a8381dc repositories: 12Compiles the bundle and reports every problem it contains — not the first one. Fixing errors one round-trip at a time is how a validator becomes something people stop running.
Run it in the policy repository’s CI.
explain
Section titled “explain”nitctl policy explain ./policy -repo backend-api -user alice -path secrets/prod.envnitctl policy explain ./policy -repo backend-api -user alice -path src/main.go -action writenitctl policy explain ./policy -repo backend-api -user alice -path src/main.go -ref refs/heads/main| Flag | ||
|---|---|---|
-repo | required | Repository id |
-user | required | User id |
-path | required | Repository-relative path |
-action | every action | read, write, create, delete or admin |
-ref | Fully qualified ref, e.g. refs/heads/main |
alice on secrets/prod.env (backend-api)groups: backend
DENY read denied by rule secrets-are-platform-only Production secrets are owned by the platform team. DENY write denied by rule secrets-are-platform-only ...The bundle directory comes first, before the flags. Go’s flag package stops
parsing at the first non-flag argument, so a positional after the flags would be
silently ignored — a silence that would make explain lie.
The decision names the rule that produced it, in both directions: a grant is as worth attributing as a denial.
nitctl policy show ./policyLists every repository, its remote and default branch, and every rule with its effect, subject, paths and actions. This is the bundle as nit compiled it, not as you wrote it — which is the version worth checking.
nitctl migrate
Section titled “nitctl migrate”nitctl migrate # uses the configured database.urlnitctl migrate -dsn postgres://nit@localhost/nitnitctl migrate -status # list migrations without applying anything| Flag | ||
|---|---|---|
-dsn | database.url, then $NIT_DATABASE_URL | PostgreSQL DSN |
-status | List, do not apply |
Migrations are never applied at start-up. A schema change is a deployment step an operator decides to take; a server that migrated on boot would happily run half-rolled-out DDL from several replicas at once.
nitctl token
Section titled “nitctl token”nitctl token create -user alice -label laptop -ttl 720hnitctl token list -user alicenitctl token revoke -id 7c1e9f2a-…| Flag | Default | |
|---|---|---|
-user | Policy user id (create, list) | |
-label | Free text, typically a machine name | |
-ttl | 720h | How long it stays valid |
-id | Session id (revoke) | |
-dsn | database.url | PostgreSQL DSN |
-policy | policy.dir | Policy bundle directory |
session: 7c1e9f2a-3b4c-4d5e-8f90-a1b2c3d4e5f6user: aliceexpires: 2026-08-30T12:00:00Z
nit_R4nD0m…
This token is shown once and is not recoverable. Store it with: nit login <server-url>Only the SHA-256 is stored. There is no recovery path: a lost token is reissued, not retrieved.
A token can only be issued to someone the bundle declares, so a typo produces an error rather than a credential for an account that authorizes nothing.
Issuing is an operator action rather than self-service. A device flow against the forge is the obvious next step; until then the trust chain stays short enough to reason about.
nitctl stats
Section titled “nitctl stats”export NIT_SERVER=https://nit.example.comexport NIT_TOKEN=nit_…
nitctl statsnitctl stats -jsonpolicy: sha256:f6040b6d6a8381dcrepositories: 12
queued: 3running: 1succeeded: 1847failed: 2
busy branches: 1denials (24h): 6Two numbers mean something is wrong right now: queued climbing, and busy branches staying high.
nitctl tasks
Section titled “nitctl tasks”nitctl tasksnitctl tasks -state queuednitctl tasks -state failed -limit 20 -jsonnitctl tasks -repository backend-api -kind push| Flag | Default | |
|---|---|---|
-state | all | queued, running, succeeded, failed |
-kind | both | push or pull |
-repository | all | Repository id |
-limit | 50 | Maximum rows |
-json | Raw JSON |
TASK KIND STATE USER REPOSITORY@BRANCH DURATION NOTEbe59af45-9694-416e-ace2-da5cffc7f145 push running carol backend-api@feature/x 2m14s worker-256c8b3f7-a0a1-4971-b5ba-3f18389b08bb push failed dave data-platform@main 1.2s conflictNOTE carries the error code for a failure, the queue position for a wait, or
the worker holding the lease for a running task.
nitctl audit
Section titled “nitctl audit”nitctl audit -limit 20nitctl audit -user bob -since 24hnitctl audit -repository backend-api -since 168hnitctl audit -request 01J8Z3Q2M7C4V9K1 -json| Flag | Default | |
|---|---|---|
-user | all | Actor |
-repository | all | Repository id |
-request | Request id — follows one operation end to end | |
-since | all time | A duration, e.g. 24h |
-limit | 50 | Maximum rows |
-json | Raw JSON |
WHEN ACTOR ACTION REPOSITORY@BRANCH PATH RULE2026-07-31 00:35:29 bob push.applied backend-api@main2026-07-31 00:33:14 bob push.denied_path backend-api@main secrets/prod.env secrets-are-platform-onlyEnvironment
Section titled “Environment”NIT_SERVER | Server for stats, tasks, audit |
NIT_TOKEN | Admin token for the same |
NIT_DATABASE_URL | Fallback DSN for migrate and token |
NIT_POLICY_DIR | Fallback bundle directory for token |
NIT_CONFIG | Configuration file to read |
With NIT_SERVER set and no NIT_TOKEN, nitctl reuses the credential
nit login stored — so an operator with a developer install does not paste a
token twice.
not found
Section titled “not found”nitctl: not found; is this account in one of the server's NIT_ADMIN_GROUPS?The token is valid; the account is not in server.admin_groups. It is a 404
rather than a 403 deliberately: the existence of an operations API is not
something an ordinary developer needs confirmed.
- Configuration settings — every key.
- Day-to-day operations — these commands in context.