First-boot configuration must be deterministic and idempotent: /etc/uci-defaults scripts run at boot S10 and are deleted only when they exit success (boot uci_apply_defaults, boot L7-L20). Anything non-idempotent will re-run forever. All mechanics below are verified against the local OpenWrt source snapshot (see Offline Baseline).
Determine Task Type
| If the user wants to... | Path |
|---|---|
| Change default UCI values on first boot of a flashed image | uci-defaults script → Workflow A |
Ship a complete known-good /etc/config/<pkg> from scratch |
Preseeded final file → Workflow B |
| Diagnose "my uci-defaults runs every boot" | Workflow C |
| Make uHTTPd serve a custom docroot/CGI | Workflow A + uHTTPd Config |
| Write an init.d service script | Hand-off → openwrt-procd-init |
Prerequisites / Preflight
On the target device (or its image manifest):
cat /etc/openwrt_release # release matches baseline below
ls -l /etc/uci-defaults/ # leftover scripts = pending/failed work
uci show uhttpd.main 2>/dev/null | head # current uHTTPd state
uci -q get firewall.@zone[0].name # expect: lan (input ACCEPT)
No device in hand → deliver scripts + verification commands marked Build Verification Only. Do not claim first-boot-tested behavior.
Offline Baseline
Verified 2026-09-02 against the local OpenWrt source snapshot (workspace upstream-refs/openwrt; convention baseline OpenWrt 25.12.5). Key sources: package/base-files/files/etc/init.d/boot (uci_apply_defaults), package/network/services/uhttpd/files/uhttpd.config + uhttpd.init, package/network/config/firewall/files/firewall.config. TinyNAS overlay samples: openwrt-imagebuilder/common/tinynas-files/. This skill does not auto-update.
Contracts
/etc/uci-defaults/<NN-name>scripts are plain#!/bin/sh, run once at boot S10 inls(lexicographic) order, inside a subshell per file (boot L14-L16).- Success = the file's exit status is 0. Only then is it appended to the delete list;
uci commitruns once after all files, thensync, then successful files are removed (boot L14-L19). Failures stay and re-run next boot. - Idempotence is mandatory: every script must tolerate being executed any number of times, and must end with
exit 0even on "already done" paths. - The lan firewall zone ships
input ACCEPT(firewall.config L9-L14): uHTTPd 80/443 and dropbear 22 are reachable from LAN with zero extra firewall config. Do not add redundant "allow 80 from lan" rules. - First-boot behavior claims without a device test are
Pending HIL.
Capability Boundaries and Hand-off
Read only the reference needed for the current task:
| Task | Reference |
|---|---|
| Lifecycle details, failure semantics, ordering, why files re-run | uci-defaults Lifecycle |
| uHTTPd UCI option table, handler precedence, firewall reachability | uHTTPd Config |
| Preseeded final file vs uci-defaults delta trade-offs | Preset vs Defaults |
Beyond-scope intents route to another skill:
| User intent | Route to |
|---|---|
| init.d service scripts, procd instances, START ordering | openwrt-procd-init |
Overlay layout, FILES=, DISABLED_SERVICES, image build determinism |
openwrt-image-build |
| Device flashing/recovery, U-Boot, TTL console | openwrt-serial-recovery |
Workflow A — idempotent uci-defaults script
- Name it
<NN>-<purpose>(e.g.50-tinynas-uhttpd); order matters only between your own scripts — they run inlsorder. - Guard what you touch, write only missing values, and always exit 0 (verified TinyNAS pattern,
etc/uci-defaults/50-tinynas-uhttpd):
#!/bin/sh
# 50-tinynas-uhttpd — first-boot idempotent config (deleted by init.d/boot on success)
[ -f /etc/config/uhttpd ] || exit 0
uci -q get uhttpd.main.rfc1918_filter >/dev/null || uci set uhttpd.main.rfc1918_filter='1'
uci -q get uhttpd.main.cgi_prefix >/dev/null || uci set uhttpd.main.cgi_prefix='/cgi-bin'
uci -q get uhttpd.main.home >/dev/null || uci set uhttpd.main.home='/www'
uci commit uhttpd
exit 0
- Do not hard-write keys that a boot-time init script owns. In the TinyNAS overlay,
index_pageis managed by the S99tinynas-bootstate machine; the uci-defaults script deliberately leaves it alone. - Place the script under
files/etc/uci-defaults/in the Image Builder overlay. Packaging andFILES=mechanics →openwrt-image-build. - Verify on first boot:
ls /etc/uci-defaults/no longer lists the script,uci get uhttpd.main.homereturns the new value, and the web root answers on :80 from LAN.
Workflow B — preseeded final /etc/config file
When a package's entire config is yours from day one (e.g. the overlay's etc/config/samba4, etc/config/minidlna), ship the final file directly instead of patching via uci-defaults. Decision criteria, upgrade interactions, and the mixed-mode rules are in Preset vs Defaults.
Workflow C — "my uci-defaults script runs every boot"
ls -l /etc/uci-defaults/— if the script is still there, it failed (or was never successful) and boot keeps it by design (boot L14-L19).- Run it by hand and check the exit code:
( . /etc/uci-defaults/50-mine ); echo $?. Any non-zero command chain (missing file, failingucion a package not yet installed, a strayset -e) aborts beforeexit 0. - Check idempotence: run it twice by hand; the second run must still exit 0 and produce no config drift.
- Fix, then either let the next boot consume it or run
rm /etc/uci-defaults/<file>after verifying the effect manually. - Do not "solve" re-runs by making the script delete itself — deletion is boot's job conditioned on success.
Validation Gates
sh -n files/etc/uci-defaults/* # syntax gate (host)
for f in files/etc/uci-defaults/*; do sh -e "$f" && sh -e "$f"; done # double-run = idempotence probe (staged env only)
# On device after first boot:
[ -d /etc/uci-defaults ] && ls /etc/uci-defaults # expect empty (or only known-failed files)
uci get uhttpd.main.home # expect the value you set
curl -sI http://<device-ip>/ | head -1 # expect HTTP/1.1 200/301 from LAN
Host-side double-run is Build Verification Only; the authoritative gate is first boot on the target (Pending HIL until then).
Pitfalls
- Do not write a script that exits non-zero on the "already configured" path; that is the classic every-boot re-run bug.
- Do not depend on network in uci-defaults (no curl/wget/opkg). First boot may have no WAN, and pulling remote content breaks image determinism; route such needs to build-time packaging.
- Do not commit secrets (keys, passwords) into uci-defaults; they live in the image and in every backup/sysupgrade archive unless overwritten.
- Do not assume per-file
uci commitordering across scripts: boot runs one globaluci commitafter all scripts (boot L17); per-package commits inside scripts are allowed but do not replace it. - Do not patch keys owned by a boot-order init script (e.g. TinyNAS
index_pageowned by S99tinynas-boot); encode ownership in comments and pick one owner. - Do not add firewall "allow 80/22 from lan" rules — lan already has
input ACCEPT(firewall.config L12); extra rules just mislead readers. - Do not test uci-defaults with
uci seton a live box and then reuse the same script unchanged: verify the script alone reproduces the state from the package default.
Official Sources
- uci-defaults guide: https://openwrt.org/docs/guide-developer/uci-defaults
- uHTTPd documentation: https://openwrt.org/docs/guide-user/services/webserver/http.uhttpd
- Firewall configuration: https://openwrt.org/docs/guide-user/firewall/firewall_configuration
- boot script source: https://github.com/openwrt/openwrt/blob/main/package/base-files/files/etc/init.d/boot
Privacy
This skill does not collect, store, or transmit user data. uci-defaults and preseeded configs are part of the distributed image — never embed credentials, license material, or personal endpoints in them; inject secrets at activation time instead.
Supply chain red line: the uci-defaults script executes at first boot as root(S10)—any pattern of "curl remote content in a script and then execute/write it to configuration" = running unaudited remote code as root (dual supply chain + privacy risk), and it breaks image determinism. Configuration must be built in at build time.