Repository navigation
Safe On Live
← Home
How nself keeps a serving project safe from its own commands. This page covers the operation lock and the build change plan. Related pages: Compat-V15, Exit-Codes, error-codes.
One write, remote or destructive nself command runs at a time per project, across processes. A second one stops instead of racing the first over generated files, the .env cascade or the running stack.
The invocation decorator takes the lock around a command's body when all of these hold:
- A project root resolves from the current directory (the nearest folder with an
.envmarker file). Outside a project nothing is locked. - The command's registry
side_effect, after flag escalation, iswrite,remoteordestructive. For examplenself build,nself config set,nself stop --volumes,nself plug --apply. - The command's output kind is
document.streamandinteractivecommands are exempt: they can run for hours and no machine surface exposes them.nself start --watchis a stream.
Read commands (status, doctor, config get, logs, and every other side_effect: read) never take the lock and never create its file. A command refused by --json (E402) is refused before any lock is taken.
The lock is an exclusive flock on <project>/.nself/op.lock. The kernel drops it when the holder exits or is killed, so a crash (including kill -9) never leaves it held. The file stays on disk; only the flock means "held".
While held, the file contains one JSON object (contract:cli.oplock v1):
{"pid": 4242, "command": "nself build", "started_at": "2026-10-05T12:00:00Z", "token": "<32 hex>", "host": "devbox"}The lock is released when the command returns, fails or panics, and when the process dies. nself start --watch releases it before its watch loop, so a concurrent nself config set proceeds.
| Mode | A second command finds the lock held |
|---|---|
v1.5 (NSELF_V15=1) |
Fails at once with E460, exit 1, naming the holder: wait for nself build (pid 4242) or stop it. With --json the error envelope goes to stdout as usual. |
| v1.4 (default) | Waits up to 30 seconds (one notice on stderr), then prints a warning on stderr and runs without the lock. |
If the lock file cannot be created (read-only project folder) the command runs unlocked with a warning on stderr. The lock is a no-op on platforms without flock (Windows). Use WSL2 there. That platform notice prints once, and only to a terminal: never under --quiet or when stderr is a pipe or log. All notices go to stderr, never stdout.
A command that runs nself itself (nself bundle, nself admin, nself deploy, nself mcp call nself build) must not deadlock on its own lock. The holder exports its token as NSELF_OPLOCK_TOKEN. A child nself whose token equals the token in the holder file, while the lock is held, runs without taking it. A token whose lock is no longer held grants nothing. A command that is waiting and finds a holder record naming a process on this host that has already exited treats the holder as unknown and reads again, so it never names a dead pid or re-enters on a dead holder's token.
It is a cooperative lock between nself processes of the same user:
- A process that does not use nself (a shell script,
docker composeby hand, an editor) ignores it. - The token is in the holder file, so any reader of the project folder can export it and pass for a child. It stops accidents, not an adversary.
- The holder sets
NSELF_OPLOCK_TOKENin its own environment, so every child process inherits it: docker, hooks and plugins as well as a childnself. It is a cooperative token, not a secret. - A child that outlives its parent runs unlocked: the parent's lock ended with the parent.
- A
cleanthat deletes.nself/while the lock is held orphans that lock. The next command locks the new file. -
nself buildtakes this same lock (it no longer keeps its own.nself/build.lockexcept as described next), so a killed build leaves nothing stale. A build that finds the lock held by another build fails at once, naming the holder, and writes nothing. Held by another command: v1.5 fails at once naming that command; v1.4 keeps the old behaviour (the command guard waits up to 30 seconds, then the build goes ahead) but under the old O_EXCL.nself/build.lock, so two builds never overlap. Where flock is unsupported (native Windows) the O_EXCL.nself/build.lockis the lock, as before. A build never runs unlocked.reconcile.Applyholds the lock from the first render to the last write.
| Code | Exit | Meaning |
|---|---|---|
| E460 | 1 | Another nself command holds the project operation lock. |
| E461-E464 | Reserved. |
nself build --plan answers "what would this change?" before anything changes. nself build with no flags then applies that same plan. Both run the one build pipeline: the plan is the pipeline run in memory, so nothing the apply does is missing from it, with the exceptions listed under "What a plan does not cover".
- It writes nothing under the project,
~/.nself, the plugin directory, the fronting stack's nginx directory or/etc/hosts, and it starts, stops and creates no container. The command guard still creates the operation lock file.nself/op.lock(build is awritecommand), and the usual one-line invocation log in~/.nself/logs/nself.logis written unlessNSELF_CMD_LOG_ENABLED=false. The log directory is created0700and the log files0600(an older, wider directory or rotated file is narrowed on the next write). Secret arguments, secret-looking flag values (--db-password,--auth-token) and URL passwords are written as[REDACTED]; the log holds the command line and the error text, never command output. - Container impact comes from
docker compose config --hashover the planned files (the same files and env filesnself startpasses) compared with thecom.docker.compose.config-hashlabel of the running containers. A service whose hash differs isrecreate; one running without the label isunknownand treated as a recreate. A planned service with no container (running or stopped) iscreatewhen the project already has containers; on a project with none, nothing is listed ascreate. A service that mounts a named volume isstateful. Every container item isapplied_by: next-start: no build command recreates a container. If Docker cannot be asked, the plan sayscontainers.known: falseand prints a notice on stderr; it never fails for that reason. - Secrets are never printed: env-kind artifacts are redacted,
--diffshows key names only, and an effect that persists a secret names the key, not the value.
The plan is the contract:cli.change-plan v1 document (schema schemas/commands/build.v1.schema.json): plan_id, env and env_class, artifacts (changed files only, with diff_lines and generated/hand_edited), effects, containers, destructive with its reasons, requires_confirmation and empty. empty is true when the build would change nothing, which is what --plan prints after an apply.
plan_id is the sha256 of the plan's canonical JSON followed by, for every artifact in path order, the sha256 of the bytes the build would leave there and its permission bits. It binds the content of the change, not only its shape, while the JSON carries no file bytes. nself build --yes --plan-id <id> renders again and applies only if the id is unchanged; otherwise it fails with E450 and writes nothing. Swapping one env value for another of the same length changes the id.
The plan and the id cover rendered artifacts: project files, plugin compose fragments rewritten in place (shown as @plugins/<name>/docker-compose.plugin.yml), .env.secrets with the planned bytes, and permission-only changes (a 0644 .env is listed as a change with diff_lines 0). Certificate bytes and host trust state are runtime effects outside artifact byte confirmation. A plan with certificates or trust-store effects refuses --plan-id with E454 before any effects or artifact writes. Human plan and apply output disclose this exclusion, including on dev. Confirm at the prompt or use --yes without --plan-id, or provision certificates and host trust first.
A build that generates secrets (a first build, or a missing *_INTERNAL_SECRET) renders random values, which no earlier --plan run can reproduce. --plan-id is refused for it with E451 (exit 1, nothing written). Confirm at the prompt, or run --yes without --plan-id: the apply renders once and writes exactly that render.
nself build takes the project lock first, renders the project once in plan mode, builds the plan from that render, checks --plan-id, asks for confirmation, then re-renders and compares before writing: a value edited while the prompt waited is refused with E450 and nothing is written. The write itself is held to the confirmed render: artifact writes, removals and permission changes are staged, and the complete set is verified before publication. An unplanned mutation or missing expected artifact discards the staged changes, leaving project artifacts untouched. Publication saves rollback copies in a private temporary directory and uses individual atomic file replacements; an ordinary publication or post-build validation failure restores the previous artifacts. This is not an atomic replacement of the whole project, and does not guarantee recovery from process termination or a writer ignoring the lock. Runtime effects, including certificates, trust and plugin installation, remain outside this artifact transaction. Planning never changes the process environment, and the compat switch is read once at the start, so NSELF_V15 in an env file cannot turn the v1.5 refusal off.
A prod-class env is ENV prod or staging (a running stack does not make an env prod-class). A non-empty plan there needs --yes or an interactive yes. A plan that overwrites or removes a hand-edited generated file needs --force (or an interactive yes) in any env. Both refusals are v1.5 behaviour:
| Mode | Prod-class change, no --yes, not interactive |
|---|---|
v1.5 (NSELF_V15=1) |
Refused with E403 (exit 4). With --json the error envelope is on stdout. Nothing is written, and the expired-plugin removal that nself build otherwise performs does not run. |
| v1.4 (default) | The plan summary and a one-line notice go to stderr, then the build proceeds as before. |
--yes does not authorise a hand-edited overwrite; that needs --force. A destructive plan (a hand-edited file overwritten, an orphan or expired plugin removed, a stateful container recreated by the command itself) is worded strongly in the prompt and listed under destructive_reasons.
- Declared-plugin installs and expired-plugin removals change what the render reads, so they run as confirmed effects inside the held apply: the plan lists them (
plugin-install,plugin-remove) and is confirmed; the project is re-checked unchanged; the effects run; the plan of record is then rendered again, so the nginx and compose artifacts the plugins bring are planned, shown on stderr (after the plugin changes the build will write) and held like every other. An interactive prod-class apply is asked again about that render. These effects refuse--plan-idwith E453. Downloaded plugin bytes are checked against the registry SHA-256 checksum, rather than bound byplan_id; edits during the install window are included in the second render. Plugin installation and removal are runtime effects, outside the artifact transaction. A failed removal is fatal before artifact writes. - Container impact needs Docker. When it cannot be asked,
containers.knownis false, the plan is neverempty, the human output saysstate unknown, and a prod-class env asks for confirmation. - Effects (hosts, trust store, certificates) are not byte-checked at write time. Their runtime files and host state are outside artifact byte confirmation.
- Every apply writes a snapshot under
.nself/backups/nginx-sites-<timestamp>/whennginx/siteshas files (the last five are kept). The snapshot goes through the write hold: its writes and prunes are allowed only under.nself/backupsand only when the plan carries thenginx-sites-backupeffect; the plan lists that effect only when a site conf changes. - A declared plugin that
buildwould auto-install is aplugin-installeffect; the plan renders without it. - The plan is computed under the operation lock held for the whole command, so the inputs cannot change between the plan and the write by another nself command. A process that ignores the lock can still change them.
| Code | Exit | Meaning |
|---|---|---|
| E450 | 1 |
--plan-id does not match the plan this project produces now, or the project changed while the confirmation was pending. Re-run nself build --plan and pass the new id. |
| E451 | 1 | The build generates secrets, so --plan-id cannot bind it. |
| E403 | 4 | A prod-class or hand-edited change was not confirmed (v1.5). |
A generated file whose content on disk no longer matches its sha256 hash in .nself/state/generated.json is considered hand-edited.
Overwrite-on-regenerate stays the default in v1.4 mode (with a warning). In v1.5 mode, nself detects hand-edited generated files and refuses to overwrite them by default (Exit code 4, E403). It will print a unified diff of the file and require you to pass --force to forcefully overwrite your changes.
When --force is used, the file is overwritten with the newly generated content and the new hash is recorded in generated.json.
A command that changes a setting, a plugin or a bundle used to leave the generated files stale until you ran nself build. In v1.5 mode these commands plan the regeneration they cause and apply it, through the same engine as nself build (reconcile.Apply). Regeneration itself does not restart containers; the next nself start picks up the new files. nself bundle install also starts the stack and waits for app-ready in v1.5 mode.
| Command | v1.5 (NSELF_V15=1) |
v1.4 (default) |
|---|---|---|
nself config set KEY VALUE |
Plans, confirms when the env is prod-class, writes the line, regenerates nginx, compose and the rest. Prints config: KEY → <n> artifacts changed (<kinds>). If the apply fails the env file is put back byte for byte. |
Writes the line as before. Prints the same summary and not applied in v1.4 mode on stderr. Nothing is regenerated until you run nself build. |
nself plugin install |
Installs, then reconciles once after the last install, so the plugin's nginx route and compose service exist with no separate build. | Installs and prints the nself build hint. The route appears after nself build. |
nself plugin remove |
Removes, then reconciles once. | Removes. The generated files change on the next nself build. |
nself bundle install |
Installs, reconciles, starts the stack and waits for app-ready. | Installs, then reconciles once. Does not start the stack. |
nself bundle remove |
Removes, then reconciles once. | Removes, then reconciles once. |
nself doctor --fix |
Repairs generated-file drift through the same engine, then waits for boot migrations and tracks missing plugin tables in Hasura. Metadata repair requires prod/staging confirmation even without file drift. A second --fix changes nothing. |
Repairs generated-file drift only. A hand-edited file is overwritten with the v1.4 warning instead of the refusal. |
-
--preview(config set): prints the plan and writes nothing, not even the env line. -
--yes: confirms a change on a prod-class env (ENVisprodorstaging). Also accepted by plugin and bundle install and remove, and bydoctor --fix. -
--force: allows overwriting a hand-edited generated file (config set, bundle remove, doctor, build).plugin install --forcealso overrides the hand-edit refusal. Onplugin remove,--forcealso keeps its older meaning, so check the plan first. -
nself doctor --fix --planprints the generated-file repair plan and writes nothing. It does not inspect or plan live Hasura table tracking.
Both refuse with E403 and exit 4 in v1.5 mode. v1.4 prints the plan summary and a one-line notice on stderr, then proceeds as it always did.
-
Prod-class. A non-empty plan on a prod or staging env needs
--yesor a terminal yes. Forconfig set,buildanddoctor --fix, a script with no terminal and no--yesis refused before the requested files are written. Plugin and bundle install/remove change extension files and membership before this regeneration gate runs. An E403 from that gate leaves those changes in place, with generated project files still awaiting regeneration. Removal can also delete data before regeneration: v1.4 plugin removal drops its schema/tables unless--keep-data; v1.5 keeps data by default, andnself remove <plugin> --delete-datarequires separate confirmation before deleting it. After a regeneration refusal, reviewnself build --planand apply the pending change with the required confirmation. -
Hand-edit. A generated file changed since nself last wrote it is never overwritten without
--force. The diff is printed first. A conf undernginx/sitesthat does not carry the generated marker is not nself's: it is never touched, with or without--force.
The cascade integration suite (internal/reconcile/cascade_integration_test.go) drives the real binary against temp projects and a real Docker daemon. It covers a plugin install adding its route, config set BASE_DOMAIN cascading to nginx and compose, a hand-edit refused and then forced, the prod-class refusal, and an idempotent doctor --fix. Every scenario runs in v1.5 and in v1.4. It skips when Docker is missing, its daemon is unreachable, the docker compose v2 plugin is missing, or on Windows. It runs in the golden-path workflow and fails there if it is skipped.
scripts/ci/live-safety-smoke.sh is the fast check of the same guarantees. It prints one PASS or FAIL line per assertion and exits 1 on any FAIL. Lines that need Docker print SKIP when Docker, its daemon or the compose plugin is missing; a skip is never a pass.
bash scripts/ci/live-safety-smoke.sh [path-to-nself-binary]
See also cmd-config, cmd-plugin, cmd-bundle, cmd-doctor and Compat-V15.
Getting Started
Commands
- Commands, Overview
- Lifecycle: cmd-init · cmd-build · cmd-start · cmd-stop · cmd-restart · cmd-dev
- Monitoring: cmd-status · cmd-logs · cmd-health · cmd-urls · cmd-doctor · cmd-monitor · cmd-alerts · cmd-sentry · cmd-watchdog
- Data: cmd-db · cmd-backup · cmd-dr · cmd-queue · cmd-webhooks
- Config: cmd-config · cmd-service · cmd-env · cmd-promote
- Networking: cmd-ssl · cmd-trust · cmd-dns-setup
- Security: cmd-access · cmd-security · cmd-secrets
- Tenancy: cmd-tenant · cmd-billing
- Plugins: cmd-plugin · cmd-license · cmd-dogfood (extracted, CLI-R11) · cmd-k8s (extracted, CLI-R11) · cmd-encryption (extracted, CLI-R11) · cmd-waf (extracted, CLI-R11) · cmd-federation (extracted, CLI-R11) · cmd-mail (extracted, CLI-R11) · cmd-dlq (extracted, CLI-R11)
- AI: cmd-ai · cmd-claw · cmd-model
- Templates: cmd-template
- Utilities: cmd-exec · cmd-clean · cmd-reset · cmd-update · cmd-upgrade · cmd-version · cmd-admin · cmd-migrate · cmd-migrate-firebase · cmd-migrate-supabase · cmd-completion
Features
- Features, Overview
- Feature-Auth
- Feature-Storage
- Feature-Search
- Feature-Functions
- Feature-Email
- Feature-Monitoring
- Feature-Plugins
- Feature-nClaw, AI Assistant
- Feature-nChat, Messaging
- Feature-nFamily, Family Social
- Feature-nCloud, Managed Hosting
- Feature-Memory-Rooms, Knowledge Organization
- Feature-Agent-Dashboard, Agent Metrics
- Feature-Image-Generation, AI Image Generation
Configuration
- Configuration, Overview
- Config-Env-Vars
- Config-Postgres
- Config-Hasura
- Config-Auth
- Config-Nginx
- Shared-Proxy
- Config-Optional-Services
- Config-Custom-Services
- Config-System
Plugins (87 + 10 monitoring)
- Plugin-Overview
- Plugin-Catalog
- Plugin-Install
- Plugin-Licensing
- Plugin-Architecture
- Plugin-Dev-Guide
- Plugin-Migrations-Contract
Free (25)
- plugin-backup
- plugin-content-acquisition
- plugin-content-progress
- plugin-cron
- plugin-donorbox
- plugin-feature-flags
- plugin-github
- plugin-github-runner
- plugin-invitations
- plugin-jobs
- plugin-link-preview
- plugin-mdns
- plugin-mlflow
- plugin-monitoring
- plugin-notifications
- plugin-notify
- plugin-paypal
- plugin-search
- plugin-shopify
- plugin-stripe
- plugin-subtitle-manager
- plugin-tokens
- plugin-torrent-manager
- plugin-vpn
- plugin-webhooks
Pro (62)
- plugin-access-controls
- plugin-activity-feed
- plugin-admin-api
- plugin-analytics
- plugin-auth
- plugin-bots
- plugin-browser
- plugin-calendar
- plugin-cdn
- plugin-chat
- plugin-claw
- plugin-claw-budget
- plugin-claw-news
- plugin-claw-web
- plugin-cloudflare
- plugin-cms
- plugin-compliance
- plugin-ddns
- plugin-devices
- plugin-documents
- plugin-entitlements
- plugin-file-processing
- plugin-game-metadata
- plugin-geocoding
- plugin-geolocation
- plugin-google
- plugin-home
- plugin-idme
- plugin-knowledge-base
- plugin-linkedin
- plugin-livekit
- plugin-media-processing
- plugin-meetings
- plugin-moderation
- plugin-mux
- plugin-object-storage
- plugin-observability
- plugin-photos
- plugin-podcast
- plugin-post
- plugin-realtime
- plugin-retro-gaming
- plugin-rom-discovery
- plugin-social
- plugin-sports
- plugin-support
- plugin-tmdb
- plugin-voice
- plugin-web3
- plugin-workflows
Planned (26)
plugin-auditplugin-blogplugin-checkoutplugin-commerceplugin-drmplugin-exportplugin-flowplugin-importplugin-ldapplugin-mailgunplugin-mediaplugin-oauth-providersplugin-pagesplugin-postmarkplugin-rate-limitplugin-reportsplugin-samlplugin-schedulerplugin-sendgridplugin-ssoplugin-subscriptionplugin-thumbplugin-transcoderplugin-twilioplugin-wafplugin-watermark
Guides
- Guide-Production-Deployment
- Guide-SSL-Setup
- Guide-Multi-Tenancy
- Guide-Security-Hardening
- Guide-Monitoring-Setup
- Guide-Backup-Restore
- Guide-Custom-Services
- Guide-Migration-from-v1
Architecture
- Architecture
- Architecture-Guardrails
- Service-Graph
- Compose-Generation
- Nginx-Generation
- Plugin-Architecture
Reference
Machine interface
-
JSON-Output,
--jsonenvelope -
Command-Registry,
nself help --json - Exit-Codes
- error-codes, Error Codes
-
Compat-V15,
NSELF_V15gate
Licensing
Security
Brand
Operations
- operations/release-cascade, Release Cascade
- operations/self-healing, Self-Healing Schema
- operations/redis-tuning, Redis Pool Tuning
- operations/meilisearch-warmup, MeiliSearch Warm-Up
- operations/jwt-rotation, JWT Key Rotation
- operations/windows-wsl2-setup, Windows / WSL2 Setup
- operations/gemini-oauth-reauth, Gemini OAuth Reauth
Contributing
Admin
- USER-ACTION-QUEUE, Pending Admin Actions
All commands (21)
- A: cmd-add · cmd-admin
- B: cmd-backup · cmd-build
- C: cmd-clean · cmd-config
- D: cmd-db · cmd-deploy · cmd-doctor
- E: cmd-exec
- I: cmd-init
- L: cmd-license · cmd-logs
- M: cmd-mcp
- R: cmd-remove · cmd-reset · cmd-restart
- S: cmd-start · cmd-status · cmd-stop
- U: cmd-update