I have been writing about TrueNAS SCALE on this site since 2023, mostly as a homelab user poking at apps and GPU passthrough. This week I shipped something more ambitious: a Claude Code skill that lets Claude actually drive my NAS, safely, over the API that survives the next release.
🔗 Repo: github.com/jamieede123/TrueNAS-Skill — install in about 3 minutes, scoped Readonly Admin user by default, MIT licensed. Stars and issues very welcome.
Why I built it ¶
Two things lined up.
First, iX is retiring the TrueNAS REST API. It is deprecated in 25.04
and removed entirely in 26.04. Most of the example code on the internet for talking to TrueNAS programmatically uses /api/v2.0/ endpoints, and a lot of that is about to stop working. The replacement is the JSON-RPC 2.0 API over WebSocket
, which has been there for years but is far less documented in the wild.
Second, I had been doing more and more TrueNAS tasks in Claude Code by hand. Asking it to check pool health, look at snapshot tasks, summarise alerts, see which apps had updates. Every session I was either copy-pasting middlewared output back in or letting Claude guess at things that had already been removed from the API. That felt silly.
A skill solves both. It is WebSocket-only by design (so it keeps working after 26.04), and it gives Claude one consistent way to talk to the box instead of inventing curl commands per task.
What the skill actually is ¶
At its core it is a single Node script, scripts/truenas.mjs, that speaks JSON-RPC 2.0 over wss://your-nas/api/current, plus a SKILL.md that teaches Claude when to use it and how. The full source is on GitHub: jamieede123/TrueNAS-Skill
.
The script has six modes:
- Direct call.
node scripts/truenas.mjs system.inforeturns the rawresult. - Query helper. Wraps the standard
[filters, options]shape that every.querymethod takes in TrueNAS, so you can write--filter pool = tank --select name,used --limit 20instead of hand-rolling the JSON. - Job mode. Long-running methods (scrubs, app upgrades, dataset deletes) return a numeric job ID, not the result.
--jobsubscribes tocore.get_jobs, streams progress to stderr, and resolves when it finishes or fails. - Watch mode. Subscribes to any collection and emits one JSON line per event until Ctrl+C. Live alert tail, app stats, container logs, audit events.
- File I/O. TrueNAS has separate
/_uploadand/_downloadHTTP endpoints for things like config backups and manual update images. The client handles auth and the multipart form for both. - Ping. Connects, logs in, calls
core.ping, prints the authenticated user. Smoke test.
Everything else is reference files, loaded on demand. Storage, snapshots, apps, sharing, networking, virtualisation, alerts, audit, certificates, mail, scheduled tasks, updates and boot environments each get their own file under references/. Claude does not load any of these unless the user asks about that area, which keeps the context cost of the skill itself small.
Least-privilege auth, by default ¶
The first time I built something against the TrueNAS API I used a truenas_admin key, because that is what every example shows. It works, but a Full Admin key can wipe pools, delete users, reconfigure networking, reboot the box. Fine for me running a script I just wrote. Not fine for an LLM that occasionally misreads instructions.
So the skill is opinionated about this. The install instructions tell you to create a dedicated claude-skill user with Readonly Admin as the starting role under Credentials → Users
, disable its password, disable SMB, shell and SSH access, and mint the API key under that user. Five fields, ~3 minutes in the UI. The skill then verifies the key is bound to the scoped user with auth.me, and the README walks through testing that a write attempt actually fails with [EACCES] Not authorized.
Want more permissions later? Edit the user, swap Readonly Admin for a stronger role (or build a custom one binding specific role atoms like SNAPSHOT_WRITE, DATASET_WRITE, APPS_WRITE). The existing key inherits the new role on next call. No need to mint a new key.
If you want the full belt-and-braces version, the README also documents how to mint everything over the API itself: user.create, privilege.create, api_key.create, then throw away your bootstrap Full Admin key.
What I had to learn the hard way ¶
A few things took a couple of iterations to get right.
HTTPS is non-negotiable. TrueNAS auto-revokes API keys it sees used over plain http:// or ws://. The client now refuses to start if TRUENAS_URL is not TLS.
The per-IP rate limit is real. TrueNAS rate-limits new WebSocket connections with [EBUSY] Rate Limit Exceeded, roughly one per second. Each CLI invocation opens a fresh socket plus auth, so a tight loop in a shell script will trip it almost immediately. The client retries EBUSY once with jitter, but for proper bulk work the right answer is core.bulk, which runs the same method against many inputs in one RPC. That needs FULL_ADMIN-level privilege though, even when the wrapped method is read-only. A Readonly Admin key will get [EPERM] Not authorized. So for bulk reads on a scoped key, just loop client-side with a small sleep.
Login is one round-trip if you ask for it. Naive code calls auth.login_ex and then auth.me to find out who it just logged in as. auth.login_ex accepts a login_options.user_info flag that bundles the equivalent of auth.me into the login response. Half the round trips, much nicer for short-lived CLI calls.
Auth errors should be specific. The first version of the client printed a generic “login failed”. Now it surfaces the spec’s response_type values as actionable messages. AUTH_ERR tells you to check TRUENAS_USERNAME matches the user that owns the key. EXPIRED tells you to mint a new key. OTP_REQUIRED tells you to set TRUENAS_OTP. REDIRECT tells you the box wants you to point at a different node (usually HA failover) and gives you the target URL.
PowerShell strips your quotes. This one took me longer than I want to admit. '[{"name":"tank/foo"}]' works fine in bash and zsh, but PowerShell removes the inner " before Node ever sees the argument, and you get Expected property name. The fix was to support - as a sentinel meaning “read params from stdin”, so PowerShell users can pipe a here-string in and the JSON survives. The client detects the shell-stripped-quotes failure mode and prints the working alternative.
What it feels like to use ¶
The point of the skill is that you stop thinking about any of the above. Once it is installed, the interactions read like normal Claude Code chat:
“How is my NAS doing?”
Claude runs pool.query filtered on health, alert.list, app.query filtered on upgrade_available, and replication.query, and gives me a one-paragraph summary with anything red called out at the top.
“Take a snapshot of tank/data called manual-2026-05-28.”
One call to pool.snapshot.create. Done.
“Scrub the tank pool and tell me when it finishes.”
--job pool.scrub.scrub with the right params. Progress streams to stderr. Claude waits for SUCCESS and reports the result.
“Tail alerts as they come in.”
--watch alert.list. JSON line per added, changed, or removed alert event.
“Which apps have an update available, and what is the new version?”
Query helper does the lift in one call: --query app.query --filter upgrade_available = true --select name,version.
The skill is opinionated about destructive operations too. The SKILL.md has an explicit list of methods that change or destroy state (pool.dataset.delete, app.delete, system.reboot, config.upload, anything matching *.update on a config Claude has not read first) and instructs Claude to print what it is about to do and ask before running. I would rather a brief “are you sure?” round-trip than a confidently rebooted NAS.
What is next ¶
A few things I want to add but did not need for the first cut:
- A small set of recipes for common multi-step workflows (install app from catalog, replicate dataset to another pool, rotate the API key the skill is using itself).
- Better defaults for
app.container_log_follow, including ANSI stripping built in rather than left as a note. - A worked example of the
audit.queryshape, because the wrapper-object format withservices,query-filtersandquery-optionsis genuinely confusing the first time you see it (and there is already areferences/audit.mdcovering it, but a screencast would help).
If you run TrueNAS SCALE and use Claude Code, I would love it if you tried this and told me what broke. Five minutes to install, scoped Readonly Admin user by default, no Full Admin keys in .env files.
⭐ Grab it here: github.com/jamieede123/TrueNAS-Skill — issues, PRs and “this broke on my box” reports all welcome.
And if the API key in your .env is still under truenas_admin, please go and fix that today regardless of whether you ever install this skill. The new role system in 25.04+
is genuinely good, and a leaked Full Admin key is one of those bugs you do not want to find the hard way.