Read the following files and report any discrepancies between the documentation and the actual script headers. The goal is to find places where the README or Manual says something that no longer matches what the script header says (wrong schedule, wrong config vars, wrong behavior description, missing scripts, extra scripts, etc.).

Files to read:
1. /boot/config/plugins/varaverk/Tools/README-Tools.md
2. /boot/config/plugins/varaverk/Tools/Manual-Tools.md
3. Script headers (top ~100 lines each):
   - /boot/config/plugins/varaverk/Tools/ramdisk_stop.sh
   - /boot/config/plugins/varaverk/Tools/smart_long_test.sh
   - /boot/config/plugins/varaverk/Tools/claude_startup.sh
   - /boot/config/plugins/varaverk/Tools/bandwidth_test.sh

Report: list each discrepancy as "FILE: what the doc says vs what the header says". If everything matches, say so. Be specific — quote the conflicting text. Keep the report under 400 words.


- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


I'll read all the specified files to compare the documentation with the actual script headers.


1	# ━━━━━ TOOLS ━━━━━
2
3	**Situational utilities — run when something needs fixing, not on a schedule.**
4	Recovery, repair, migration, and inspection tools for situations that arise outside
5	the scheduled maintenance model. These scripts sit ready for the moment you need them.
6
7	> **None of these scripts run on a schedule.** A script belongs here when it solves
8	> a specific operational situation — something you run in response to a problem, before
9	> a risky operation, or during a one-time setup task. Having a dedicated folder keeps
10	> the other folders clean and makes it obvious what runs routinely vs. situationally.
11
12	---
13
14	## ━━━ THE PROBLEM THAT BUILT THIS ━━━
15
16	**Fallback State Stuck After Testing**
17	Run a fallback test, something exits uncleanly, state file shows `FALLBACK`.
18	`fallback.sh` resumes and reads FALLBACK — starts containers it shouldn't, makes
19	decisions based on a state that doesn't reflect reality. Manual recovery means
20	knowing the exact file format and every field to reset. At 2am after a failed test.
21	Fix: `fallback_state_reset.sh` — shows current state, prompts for confirmation,
22	resets cleanly to NORMAL.
23
24	**Container Stuck on Watchdog Skip List After Fixing the Problem**
25	Authelia hit the restart loop limit — went on the skip list. Problem fixed. But
26	the watchdog still isn't monitoring it because the skip list persists on `/boot/config`
27	across reboots. Where's the file? What format? How do you clear restart history?
28	Fix: `watchdog_skip_list_manager.sh` — shows the skip list and which containers are
29	running vs. stopped, clears specific containers with confirmation.
30
31	**Emby Crashing With No Clear Cause After a Power Cut**
32	Server lost power with Emby running. Emby comes back, runs for 20 minutes, crashes.
33	Logs show database errors. Which database? library.db? users.db? Each has different
34	recovery implications — deleting the wrong one resets all user watch history.
35	Fix: `emby_database_repair.sh` — stops Emby, runs `PRAGMA integrity_check` on every
36	database, reports per-database with specific guidance on what to do about each one.
37
38	**Files Owned by Root After an Admin Copy**
39	`scp` a file into a media share. File arrives as `root:root`. Radarr fails to import —
40	permission denied. The daily permissions script won't run for another 20 hours. Running
41	`media_shares_permissions.sh` on the whole library takes 30 minutes just to fix one dir.
42	Fix: `bulk_permissions_repair.sh` — takes specific paths, applies correct ownership and
43	permissions in seconds.
44
45	**No Way to Back Up a Container Before a Risky Update**
46	Major version update, changelog says "database migration — no rollback." You want a
47	point-in-time backup. But `cp -r` while the container is running produces an
48	inconsistent backup, and tar without stopping the container is equally unreliable.
49	Fix: `container_data_export.sh` — stops the container cleanly, archives appdata to a
50	timestamped `.tar.gz`, verifies archive integrity, restarts the container.
51
52	**Fresh HOST2 Has Shares Configured But Directories Missing**
53	Fresh install on HOST2. Restored `/boot/config/shares/` from backup. Array starts.
54	Shares show in the UI. But the actual `/mnt/diskN/sharename` directories don't exist —
55	unRAID created the share definitions but not the directories. rsync.sh aborts.
56	Fix: `recreate_shares.sh` — reads every `.cfg` file, creates directories on each
57	included disk, places `.recovery` markers so the first rsync won't delete anything.
58
59	---
60
61	## ━━━ WHAT THIS FOLDER DOES ━━━
62
63	One role: hold scripts for situations the scheduled maintenance model can't handle.
64
65	Every script here was written because a specific situation arose that required bash
66	commands to resolve — and that situation is guaranteed to arise again. When you encounter
67	something new, write the tool. Store it here. Find it at 2am next time.
68
69	**Recovery Tools** — Restore known-good state after a failure
70	`fallback_state_reset.sh`, `watchdog_skip_list_manager.sh`
71
72	**Diagnostic Tools** — Inspect and verify before acting
73	`emby_database_repair.sh`, `continuous_scripts_status.sh`
74
75	**Repair Tools** — Fix a specific known problem
76	`bulk_permissions_repair.sh`, `zfs_pool_scrub.sh`
77
78	**Lifecycle Tools** — Backup, setup, and migration support
79	`container_data_export.sh`, `recreate_shares.sh`, `claude_startup.sh`, `ramdisk_stop.sh`
80
81	**Library Sync Bootstrap** — Close the gap between Emby and arr libraries
82	`emby_to_lidarr_sync.sh`, `emby_to_sonarr_sync.sh`, `emby_to_radarr_sync.sh`
83
84	---
85
86	## ━━━ RELATIONSHIP TO OTHER FOLDERS ━━━
87
88	```
89	System_Essentials/  ← regular system maintenance    — scheduled
90	Docker_Essentials/  ← regular container management  — scheduled
91	Monitors/           ← regular health reporting       — scheduled
92	Orchestrators/      ← regular maintenance windows   — scheduled
93	Fallback/           ← automated fallback/handback   — event-driven
94	Tools/              ← situational utilities          — run when needed
95	```
96
97	Some tools interact with state written by other folders:
98
99	```
100	Fallback/
101	  fallback.sh ──────── writes FALLBACK_STATE_FILE ──► fallback_state_reset.sh reads/writes it
102
103	Docker_Essentials/
104	  docker_watchdog.sh ── writes skip list + history ──► watchdog_skip_list_manager.sh manages them
105
106	Docker_Essentials/ + System_Essentials/ + Fallback/
107	  All continuous scripts ──────────────────────────► continuous_scripts_status.sh reads their state
108	```
109
110	Tools never call scripts in other folders. Other folders never call Tools scripts.
111	The relationship is one-way: Tools act on state that other scripts have written.
112
113	---
114
115	## ━━━ SCRIPTS IN THIS FOLDER ━━━
116
117	| Script | What It Fixes | When to Run |
118	|--------|--------------|-------------|
119	| `fallback_state_reset.sh` | State file stuck in FALLBACK after test or failed handback | After fallback testing or manual intervention |
120	| `watchdog_skip_list_manager.sh` | Container stuck on watchdog skip list after fixing root cause | After fixing a container that hit the restart loop limit |
121	| `bulk_permissions_repair.sh` | Files owned by wrong user after admin copy or bad container config | When arr operations fail due to permissions |
122	| `container_data_export.sh` | Need a clean backup before a risky container update or migration | Before major updates, appdata migrations, or container removals |
123	| `emby_database_repair.sh` | Emby crashing with database errors after power loss or crash | When Emby logs show corruption or repeated crashes |
124	| `zfs_pool_scrub.sh` | Verify ZFS pool integrity — catch silent corruption before it spreads | Monthly, or after any disk or power event |
125	| `recreate_shares.sh` | Share directories missing after fresh install or disk rebuild | After fresh unRAID install or disk replacement on HOST2 |
126	| `continuous_scripts_status.sh` | Need a live view of watchdog and fallback state | Any time — manual dashboard, no schedule |
127	| `claude_startup.sh` | Claude Code session setup after reboot — symlinks persistent storage | After each unRAID reboot, or called by array_started.sh |
128	| `ramdisk_stop.sh` | Safely stop the transcode ramdisk — redirect symlink to SSD, unmount, update state | Before re-running ramdisk_setup.sh with new size or thresholds |
129	| `emby_to_lidarr_sync.sh` | Add all Emby album artists not yet tracked in Lidarr | After Lidarr setup, database wipe, or when you suspect gaps |
130	| `emby_to_sonarr_sync.sh` | Add all Emby TV series not yet tracked in Sonarr | After Sonarr setup, database wipe, or when you suspect gaps |
131	| `emby_to_radarr_sync.sh` | Add all Emby movies not yet tracked in Radarr | After Radarr setup, database wipe, or when you suspect gaps |
132
133	---
134
135	## ━━━ HOW THE SCRIPTS RELATE ━━━
136
137	All Tools scripts are independent — none call each other, none are called by other Tools.
138
139	```
140	Situation arises
141	      │
142	      ▼
143	┌──────────────────────────────────────────────────────────────────────┐
144	│  Tools/                          Run directly when needed             │
145	│                                                                       │
146	│  fallback_state_reset.sh     ◄── after fallback test / failed handback│
147	│  watchdog_skip_list_manager  ◄── after fixing a crash-looping container│
148	│  bulk_permissions_repair     ◄── wrong ownership after copy or rsync  │
149	│  container_data_export       ◄── before a risky update or migration   │
150	│  emby_database_repair        ◄── Emby logs show corruption            │
151	│  zfs_pool_scrub              ◄── monthly integrity check / post-event │
152	│  recreate_shares             ◄── fresh HOST2 setup or disk rebuild    │
153	│  continuous_scripts_status   ◄── manual status check at any time     │
154	│  claude_startup              ◄── after each unRAID reboot             │
155	│  ramdisk_stop                ◄── before ramdisk resize / remount      │
156	│                                                                       │
157	│  emby_to_lidarr_sync         ◄── Lidarr setup / database wipe / gap  │
158	│  emby_to_sonarr_sync         ◄── Sonarr setup / database wipe / gap  │
159	│  emby_to_radarr_sync         ◄── Radarr setup / database wipe / gap  │
160	└──────────────────────────────────────────────────────────────────────┘
161	      │
162	      ▼
163	  State files in other folders (Fallback/, Docker_Essentials/) may be read or written.
164	  No other scripts call into Tools/.
165	```
166


1	# ━━━━━ TOOLS — Manual ━━━━━
2
3	Configuration reference, usage procedures, and field guides for every script
4	in `Tools/`. Run any script with `--status` first — it shows current state before
5	making any changes.
6
7	---
8
9	## ━━━ CONTENTS ━━━
10
11	- [emby_to_lidarr_sync.sh](#emby_to_lidarr_syncsh)
12	- [emby_to_sonarr_sync.sh](#emby_to_sonarr_syncsh)
13	- [emby_to_radarr_sync.sh](#emby_to_radarr_syncsh)
14	- [fallback_state_reset.sh](#fallback_state_resetsh) *(not yet built — manual workaround)*
15	- [watchdog_skip_list_manager.sh](#watchdog_skip_list_managersh)
16	- [bulk_permissions_repair.sh](#bulk_permissions_repairsh)
17	- [container_data_export.sh](#container_data_exportsh)
18	- [emby_database_repair.sh](#emby_database_repairsh)
19	- [zfs_pool_scrub.sh](#zfs_pool_scrubsh)
20	- [recreate_shares.sh](#recreate_sharessh)
21	- [continuous_scripts_status.sh](#continuous_scripts_statussh)
22	- [claude_startup.sh](#claude_startupsh)
23	- [ramdisk_stop.sh](#ramdisk_stopsh)
24	- [Adding a New Tool](#adding-a-new-tool)
25
26	---
27
28	## Output Tiers
29
30	All tools have two output levels controlled by `--log`.
31
32	Without `--log`, each script processes and always concludes with a summary block
33	showing identity, duration, counts, and a status line. Warnings and errors are
34	always visible. State-display scripts (watchdog_skip_list_manager, zfs_pool_scrub
35	`--status`, fallback_state_reset current-state section) always show their state
36	output — `--log` adds configuration detail and per-item resolution within each
37	section.
38
39	With `--log`, per-item detail appears: individual items added/skipped, per-database
40	check results, per-pool scan lines, per-container stop/start state, per-directory
41	creation results. Use when debugging unexpected results or confirming a first run.
42
43	---
44
45	## emby_to_lidarr_sync.sh
46
47	One-shot bootstrap tool. Scans Emby play history, finds artists you've actually
48	listened to that are not yet tracked in Lidarr, and adds them. No scoring — if
49	it was played, Lidarr should monitor it. Not scheduled; run manually when you
50	want to close the gap between what's in your library and what Lidarr watches.
51
52	### When to Use
53
54	- After initial Lidarr setup — bring it in line with existing listening history
55	- After a Lidarr database wipe or migration
56	- Any time you suspect artists you listen to are slipping through unmonitored
57
58	### Usage
59
60	```bash
61	# See what would be added (no changes)
62	bash Tools/emby_to_lidarr_sync.sh --dry-run
63
64	# Limit to recent plays only
65	bash Tools/emby_to_lidarr_sync.sh --dry-run --days 30
66
67	# Run for real
68	bash Tools/emby_to_lidarr_sync.sh
69	```
70
71	### Notes
72
73	- Reads all MusicAlbum items from Emby and extracts AlbumArtist — primary album
74	  artists only, not guest features or tag credits
75	- Filters out VA, Various Artists, and other metadata placeholders
76	- Triggers `ArtistSearch` immediately after each successful add — no manual search needed
77	- Activity log has a finite history; use `--days N` if the log has been pruned
78
79	---
80
81	## emby_to_sonarr_sync.sh
82
83	One-shot bootstrap tool. Finds TV series present in Emby that are not tracked in
84	Sonarr and adds them. Uses TVDB ID matching when available (more reliable than
85	title matching), falling back to case-insensitive title comparison.
86
87	### When to Use
88
89	- After initial Sonarr setup — bring it in line with your existing library
90	- After a Sonarr database wipe or migration
91	- Any time series you own are slipping through unmonitored
92
93	### Usage
94
95	```bash
96	# See what would be added (no changes)
97	bash Tools/emby_to_sonarr_sync.sh --dry-run
98
99	# Run for real
100	bash Tools/emby_to_sonarr_sync.sh
101	```
102
103	### Notes
104
105	- Triggers `SeriesSearch` immediately after each successful add — Sonarr begins
106	  searching for missing episodes right away
107	- Adds to the first accessible root folder in Sonarr
108	- TVDB ID match preferred over title; title fallback handles edge cases
109	- Requires `SONARR_EMBY_LIBRARIES` configured in `master.conf`
110
111	---
112
113	## emby_to_radarr_sync.sh
114
115	One-shot bootstrap tool. Finds movies present in Emby that are not tracked in
116	Radarr and adds them. Uses TMDB ID matching when available, falling back to
117	case-insensitive title comparison.
118
119	### When to Use
120
121	- After initial Radarr setup — bring it in line with your existing library
122	- After a Radarr database wipe or migration
123	- Any time movies you own are slipping through unmonitored
124
125	### Usage
126
127	```bash
128	# See what would be added (no changes)
129	bash Tools/emby_to_radarr_sync.sh --dry-run
130
131	# Run for real
132	bash Tools/emby_to_radarr_sync.sh
133	```
134
135	### Notes
136
137	- Triggers `MoviesSearch` immediately after each successful add — Radarr begins
138	  searching for the movie right away
139	- Adds to the first accessible root folder in Radarr
140	- TMDB ID match preferred over title; title fallback handles edge cases
141
142	---
143
144	## fallback_state_reset.sh
145
146	> **Not yet built.** Use the manual workaround below.
147
148	Planned: reset the fallback state file to NORMAL and clear all tier flags. State file
149	only — does NOT start or stop any containers.
150
151	### When to Use
152
153	```
154	After fallback_test.sh didn't complete cleanly
155	  → state left in FALLBACK but containers are actually back to normal
156
157	After a failed handback
158	  → state shows FALLBACK but remote is back up and containers are split
159
160	After killing fallback.sh directly (not gracefully via SIGTERM)
161	  → state is unknown, cycle was interrupted mid-operation
162
163	After a dev/debug session
164	  → state left in a non-NORMAL state from testing
165	```
166
167	### Manual Workaround
168
169	```bash
170	# Verify before resetting:
171
172	# Right containers on right server?
173	continuous_scripts_status.sh        # shows fallback current state
174
175	# DDNS pointing correctly?
176	nslookup Gmer4Lfe.com               # confirm it resolves to the right IP
177
178	# fallback.sh not running?
179	pgrep -f "fallback.sh"              # empty output = not running
180
181	# Both servers Tailscale connected?
182	tailscale status                    # both hosts should show active
183
184	# Check current state file:
185	cat /boot/config/fallback_state.db
186
187	# Reset to NORMAL (only after confirming containers and DDNS are correct):
188	echo "state=NORMAL" > /boot/config/fallback_state.db
189	```
190
191	Resetting during an actual fallback causes fallback.sh to think everything is normal
192	and stop covering the remote — services go offline until the next detection cycle.
193
194	### What the State File Contains
195
196	```bash
197	state=NORMAL
198	fallback_start=0
199	handback_strikes=0
200	tier2_started=false
201	tier3_started=false
202	tier4_started=false
203	```
204
205	---
206
207	## watchdog_skip_list_manager.sh
208
209	View and manage the persistent container skip list used by `docker_watchdog.sh`.
210
211	### When to Use
212
213	```
214	docker_watchdog.sh restarts the same container N times within the rolling window
215	→ container added to skip list on /boot/config/
216	→ critical notification sent
217	→ watchdog stops touching it entirely
218
219	You fix the underlying problem (database, config, dependencies).
220	You need to clear the container from the skip list so monitoring resumes.
221	```
222
223	### Recovery Workflow
224
225	```bash
226	# 1. Understand the situation — always start here:
227	watchdog_skip_list_manager.sh --status
228	# Shows: skip list contents, which are running vs. stopped, restart history
229
230	# 2. Fix the underlying problem first
231	#    Check logs:    docker logs ContainerName --tail 100
232	#    Check disk:    df -h /mnt/user
233	#    Check db:      docker exec ContainerName sqlite3 /path/to.db ".tables"
234
235	# 3. Clear from skip list + restart history:
236	watchdog_skip_list_manager.sh --clear ContainerName
237
238	# 4. Start the container manually — confirm your fix worked:
239	docker start ContainerName
240
241	# 5. Watchdog resumes normal monitoring on next cycle — no further action needed
242	```
243
244	### State Files Managed
245
246	```bash
247	# Both live on /boot/config — survive reboots intentionally.
248	# A container that was skip-listed before a reboot is still broken after it.
249
250	$SYS_WATCHDOG_FAILED_FILE          # persistent skip list
251	$WATCHDOG_CONTAINER_RESTART_LOG    # restart loop tracking
252	```
253
254	### Configuration (master.conf)
255
256	```bash
257	WATCHDOG_CONTAINER_RESTART_LIMIT=3     # restarts before skip-listing
258	WATCHDOG_CONTAINER_RESTART_WINDOW=1    # rolling window in hours
259	```
260
261	### Usage
262
263	```bash
264	watchdog_skip_list_manager.sh                        # show status (default)
265	watchdog_skip_list_manager.sh --status               # explicit status
266	watchdog_skip_list_manager.sh --clear ContainerName  # clear specific + restart history
267	watchdog_skip_list_manager.sh --clear ContainerName --force   # no confirmation prompt
268	watchdog_skip_list_manager.sh --clear-all            # clear everything
269	watchdog_skip_list_manager.sh --clear-all --force    # non-interactive
270	watchdog_skip_list_manager.sh --dry-run              # preview any clear action
271	```
272
273	---
274
275	## bulk_permissions_repair.sh
276
277	Applies correct ownership and permissions to specific paths. Faster than running
278	`media_shares_permissions.sh` which processes every configured share — use this when
279	you know exactly what needs fixing and don't want to wait for a full library walk.
280
281	### When to Use
282
283	```
284	Admin copy left root:root files       — scp, cp, direct file transfer
285	New share needs permissions now       — can't wait for nightly run
286	Container wrote as root               — before PUID/PGID was fixed
287	Specific directory has wrong perms    — targeted fix, not a full library walk
288	```
289
290	Use the full `media_shares_permissions.sh` instead for:
291	- Regular nightly maintenance (already scheduled in daily_sync_maintenance.sh)
292	- After confirming a container's PUID/PGID is now correct
293	- Initial permissions setup on a new server
294
295	### Diagnosing High Wrong-Owner Counts
296
297	The script counts files with wrong ownership before applying the fix. A high count
298	on a share that was recently written means a container has wrong PUID/PGID.
299
300	```bash
301	# Fix: add to the container's Docker template:
302	PUID=99
303	PGID=100
304
305	# Common culprits writing as root:
306	#   SABnzbd, qBittorrent, slskd — check each one's Docker env vars
307	```
308
309	### Configuration (master.conf)
310
311	```bash
312	PERMISSIONS_OWNER="nobody:users"    # matches PUID=99 PGID=100
313	PERMISSIONS_DIR_MODE="755"          # directories — enter, list, no world-write
314	PERMISSIONS_FILE_MODE="664"         # files — owner+group rw, others read-only
315	```
316
317	### Usage
318
319	```bash
320	# Single path:
321	bulk_permissions_repair.sh /mnt/user/Movies
322
323	# Multiple paths — all corrected in one run:
324	bulk_permissions_repair.sh /mnt/user/Movies /mnt/user/Tv_Shows /mnt/user/Music
325
326	# Dry run first — shows count of files with wrong ownership per path:
327	bulk_permissions_repair.sh /mnt/user/Movies --dry-run
328
329	# Verbose — show each corrected file:
330	bulk_permissions_repair.sh /mnt/user/Movies --log
331	```
332
333	---
334
335	## container_data_export.sh
336
337	Exports a container's appdata directory to a compressed tar archive. Stops the
338	container first for a clean consistent backup, verifies the archive after creation,
339	then restarts the container.
340
341	### When to Use
342
343	```
344	Before major container updates — especially "database migration — no rollback" changelogs
345	Before pool migrations — clean backup before moving appdata to a new pool
346	Before removing a container from the stack — archive its data before deletion
347	Manual point-in-time backup before risky config changes
348	```
349
350	### Export Sequence
351
352	```
353	1. Space check
354	   Estimates required space from appdata size × 1.1
355	   Aborts if output directory doesn't have enough free space
356	   Container is NOT stopped until the space check passes
357
358	2. Stop container cleanly
359	   docker stop ContainerName — graceful shutdown
360
361	3. Create archive
362	   tar -czf ContainerName_YYYY-MM-DD_HH-MM.tar.gz /path/to/appdata
363
364	4. Verify archive integrity
365	   tar --test-file archive.tar.gz — confirms archive is valid and complete
366	   If verification fails → restart container anyway, report error
367
368	5. Restart container
369	   docker start ContainerName — always happens, even if archiving failed
370	```
371
372	### Usage
373
374	```bash
375	# Syntax: container_data_export.sh ContainerName AppDataPath OutputDir
376
377	# Emby backup:
378	container_data_export.sh \
379	    Emby \
380	    /mnt/media-servers/Media_Server/Emby \
381	    /mnt/user/Backups/
382
383	# Dry run — verify space and paths without stopping anything:
384	container_data_export.sh \
385	    Emby \
386	    /mnt/media-servers/Media_Server/Emby \
387	    /mnt/user/Backups/ \
388	    --dry-run
389
390	# Output filename: Emby_2026-05-14_02-30.tar.gz
391	# Timestamped — safe to run multiple times, no overwrite
392	```
393
394	---
395
396	## emby_database_repair.sh
397
398	Stops Emby, runs SQLite `PRAGMA integrity_check` on every Emby database, and restarts.
399	Reports per-database — does NOT automatically repair. Recovery requires judgment.
400
401	### When to Use
402
403	```
404	Emby logs show database errors                     → run this first
405	Emby crashing repeatedly with no clear cause       → likely database corruption
406	Playback history or user data behaving strangely   → users.db or library.db issue
407	After a hard shutdown or power loss with Emby running → check for WAL corruption
408	```
409
410	### Recovery Guide by Database
411
412	```
413	library.db          — media library metadata: titles, seasons, episodes, artwork
414	  CORRUPT → safe to delete — Emby fully rebuilds from media files on next start
415	            Rebuild takes time (hours on large libraries) but loses nothing permanent
416
417	users.db            — user accounts, watch history, playback positions, settings
418	  CORRUPT → deleting resets ALL user accounts and watch history
419	            Check for a recent backup (weekly_sync_maintenance.sh mirrors Emby/)
420	            before deleting — restore from remote if available
421
422	authentication.db   — API keys, session tokens
423	  CORRUPT → safe to delete — API keys regenerated on restart
424	            Any connected clients will need to re-authenticate once
425
426	activity.db         — activity/access log
427	  CORRUPT → safe to delete — it's a log, losing it is acceptable
428
429	library.db-wal      — write-ahead log (uncommitted transactions)
430	  PRESENT + CORRUPT → check library.db first; WAL corruption usually means
431	                      the main library.db is also affected
432	```
433
434	### Configuration (host*.conf)
435
436	```bash
437	HOST1_EMBY_CONTAINER="Emby"    # aliased by detect_hosts() → EMBY_CONTAINER
438	HOST2_EMBY_CONTAINER="Emby"
439	```
440
441	Emby's config path is detected automatically from Docker volume mounts — no manual
442	path configuration needed.
443
444	### Usage
445
446	```bash
447	emby_database_repair.sh             # stop Emby, check all databases, restart
448	emby_database_repair.sh --dry-run   # show what would be checked, no Emby stop
449	emby_database_repair.sh --log       # verbose — show SQLite output per database
450	emby_database_repair.sh --status    # show Emby config path and database locations
451	```
452
453	---
454
455	## zfs_pool_scrub.sh
456
457	Triggers ZFS scrub on all pools (or a specific named pool) and waits for completion.
458	Notifies when done with a summary of any errors found.
459
460	### Why Run ZFS Scrub
461
462	ZFS stores a checksum with every block of data. Scrub reads every block and verifies
463	the checksum matches the stored hash. Silent data corruption can sit on disk for months
464	without triggering any error — until you try to read that specific file. By then:
465	- It may already be mirrored to HOST2 in its corrupted state
466	- The original source may no longer exist
467	- ZFS can self-repair during scrub if redundancy exists (RAIDZ or mirrors)
468
469	Run monthly. Also run after any disk replacement or power event.
470	Safe to run while the system is in use — scrub runs at low I/O priority.
471
472	### Configuration (host*.conf)
473
474	```bash
475	HOST1_ZFS_REPORT_IGNORE_POOLS=(
476	    "disk10"    # JBOD member — no redundancy, skipped from default scrub
477	    "disk9"
478	    "disk8"
479	)
480
481	HOST2_ZFS_REPORT_IGNORE_POOLS=(
482	    "cache"     # example — single-disk pool excluded from default
483	)
484	```
485
486	To scrub a pool in the ignore list, specify it by name explicitly.
487
488	### Usage
489
490	```bash
491	# Scrub all pools except those in ZFS_REPORT_IGNORE_POOLS:
492	zfs_pool_scrub.sh
493
494	# Scrub a specific pool by name — bypasses the ignore list:
495	zfs_pool_scrub.sh gaming
496
497	# Check current scrub status without starting a new one:
498	zfs_pool_scrub.sh --status
499
500	# Dry run — show which pools would be scrubbed:
501	zfs_pool_scrub.sh --dry-run
502
503	# Verbose — show scrub progress every 60s poll:
504	zfs_pool_scrub.sh --log
505	```
506
507	---
508
509	## recreate_shares.sh
510
511	Creates share directories on the correct disks after a fresh unRAID install or disk
512	rebuild. Run once on HOST2 before the first rsync from HOST1.
513
514	### When to Use
515
516	```
517	After a fresh unRAID install where /boot/config/shares/*.cfg were restored:
518	  The share definitions exist → UI shows shares → directories are missing on disk
519	  rsync.sh tries to write to /mnt/user/Movies → path doesn't exist → aborts
520
521	After a disk replacement or rebuild where share folders were lost:
522	  Replacement disk is blank → no share directories on the new disk
523	  unRAID won't create them automatically
524	```
525
526	### What It Does
527
528	```
529	For each .cfg file in /boot/config/shares/:
530	  1. Read the share name (e.g., Movies)
531	  2. Read the shareInclude list (e.g., disk1,disk2,disk5)
532	  3. Create /mnt/disk1/Movies, /mnt/disk2/Movies, /mnt/disk5/Movies
533	  4. Place a .recovery marker in /mnt/user/Movies/
534
535	The .recovery marker tells rsync.sh this is a fresh share:
536	  .recovery present → rsync WITHOUT --delete (safe — new files only, nothing removed)
537	  .recovery absent  → rsync WITH --delete (normal mirror mode)
538
539	Self-cleaning: after the first successful rsync, the source side has no .recovery file,
540	so the second nightly run deletes it from the mirror and normal --delete resumes.
541	No manual cleanup needed.
542	```
543
544	### Usage
545
546	```bash
547	recreate_shares.sh              # create all missing share directories + .recovery markers
548	recreate_shares.sh --dry-run    # show what would be created without creating
549	recreate_shares.sh --log        # verbose — show each directory created per disk
550	recreate_shares.sh --status     # show share configs and current directory state
551	```
552
553	---
554
555	## continuous_scripts_status.sh
556
557	Live status dashboard for all continuously running scripts. Read-only — makes no
558	changes to any running process, container, or state file.
559
560	### What It Shows
561
562	```
563	stability_watchdog
564	  Running state, PID, uptime, approximate cycle count
565	  Active strikes, recent restart history
566	  Live snapshot: rootfs, RAM, ZFS ARC, load, zombie count, CPU temp
567
568	docker_watchdog
569	  Running state, PID, uptime
570	  Running / stopped / unhealthy container counts
571	  Required containers status
572	  Memory-monitored containers
573	  Recent restart history + skip list
574
575	fallback (fallback.sh)
576	  Current state (NORMAL / FALLBACK / HANDBACK)
577	  Tier flags and timestamps
578	  Remote Tailscale visibility
579	```
580
581	State files are read as-is — if a script is mid-cycle, the display reflects the last
582	completed cycle, not the current in-progress state.
583
584	### Usage
585
586	```bash
587	continuous_scripts_status.sh        # show full dashboard
588	continuous_scripts_status.sh --log  # verbose output with additional detail per section
589	```
590
591	---
592
593	## claude_startup.sh
594
595	Restores Claude Code's persistent data after an unRAID reboot and optionally launches
596	Claude. Standalone script — no common.sh dependency.
597
598	### Why This Exists
599
600	unRAID's root filesystem lives in RAM — `/root/.claude` and `/root/.local` are wiped on
601	every reboot. This script symlinks both directories back to persistent appdata storage
602	at `/mnt/user/appdata/claude-code/` before launching Claude.
603
604	### First Run Migration
605
606	On first run, if persistent storage is empty, the script migrates from current live locations:
607
608	```
609	/root/.claude              → /mnt/user/appdata/claude-code/.claude
610	/root/.local/share/claude  → /mnt/user/appdata/claude-code/local/share/claude
611	```
612
613	Subsequent runs skip the migration and only create the symlinks.
614
615	### Calling from array_started.sh
616
617	To auto-restore Claude data on every boot without launching an interactive session:
618
619	```bash
620	# In /boot/config/go or array_started.sh:
621	/path/to/Tools/claude_startup.sh --setup
622	```
623
624	### Usage
625
626	```bash
627	claude_startup.sh           # set up persistent symlinks and launch Claude
628	claude_startup.sh --setup   # set up symlinks only — no launch (for array_started.sh)
629	```
630
631	---
632
633	## ramdisk_stop.sh
634
635	Safely stops the transcode ramdisk: redirects the transcode symlink to the SSD
636	fallback first (so Emby continues writing without interruption), then unmounts the
637	tmpfs and updates the state file. Primary use case is stopping the current ramdisk
638	before re-running `ramdisk_setup.sh` with new size or threshold values.
639
640	### When to Use
641
642	```
643	Bumping RAMDISK_SIZE — setup script is idempotent, skips remount if already mounted
644	  → stop first, then re-run ramdisk_setup.sh with new HOST*_RAMDISK_SIZE value
645
646	Adjusting RAMDISK_WARN_GB / RAMDISK_LOW_GB thresholds
647	  → no need to stop for threshold changes (transcode_manager reads vars live)
648	  → only needed if you're also changing the size
649
650	Temporarily freeing ramdisk RAM — reclaim tmpfs back to general memory pool
651	  → stop, restart later with ramdisk_setup.sh
652	```
653
654	### Stop Sequence
655
656	```
657	1. Redirect symlink: TRANSCODE_LINK → TRANSCODE_SSD
658	   Emby immediately writes to SSD — no broken-path window during unmount
659
660	2. Check for active transcode files on ramdisk (warn, don't block)
661	   Files in progress on the ramdisk are lost on unmount — expected for maintenance
662
663	3. Unmount ramdisk
664	   Regular umount first; if busy (directory handles only, no active writes)
665	   falls back to lazy unmount automatically
666
667	4. Update /tmp/transcode_state.db → current_target=TRANSCODE_SSD
668	   transcode_manager.sh reads this on its next cycle
669	```
670
671	### transcode_manager Warning
672
673	If `transcode_manager.sh` is running, it may flip the symlink back to the ramdisk
674	on its next cycle (once the ramdisk is unmounted, that flip will fail). Stop
675	`transcode_manager.sh` first if you need the SSD redirect to hold before remounting.
676
677	### After Stopping
678
679	```bash
680	# Update host*.conf with new size values:
681	# HOST1_RAMDISK_SIZE="10G"
682	# HOST1_RAMDISK_WARN_GB=8.5
683	# HOST1_RAMDISK_LOW_GB=7
684
685	# Remount at new size:
686	bash Transcodes/ramdisk_setup.sh
687	```
688
689	### Usage
690
691	```bash
692	ramdisk_stop.sh --status     # show mount state, symlink, active files — always check first
693	ramdisk_stop.sh --dry-run    # show what would happen without making changes
694	ramdisk_stop.sh              # stop the ramdisk
695	ramdisk_stop.sh --log        # verbose — show each step
696	```
697
698	---
699
700	## Adding a New Tool
701
702	Write the tool when you solve a problem manually with bash commands. You'll face it again.
703	The cost of writing the tool is 30 minutes. The cost of reconstructing the commands at 2am
704	is much higher.
705
706	### Checklist
707
708	```
709	✓ Header explains the specific situation that requires this tool
710	✓ Root check — most tools need root
711	✓ --dry-run support — always
712	✓ --status support — show current state before acting
713	✓ Confirmation for destructive operations (interactive YES or --force flag)
714	✓ Notify on completion — success and failure
715	✓ Leave system in clean state on any exit — trap for cleanup
716	✓ Add to README-Tools.md scripts table and HOW THE SCRIPTS RELATE diagram
717	```
718
719	### Minimal Skeleton
720
721	```bash
722	#!/bin/bash
723	# ==============================================================================================
724	# ============================= Your Tool Name ================================================
725	# ==============================================================================================
726	#
727	# PURPOSE
728	# ─────────────────────────────────────────────────────────────────────────────
729	# One sentence: what situation this solves and when to use it.
730	#
731	# ==============================================================================================
732	# OPERATIONAL SAFEGUARDS
733	# ==============================================================================================
734	#
735	# Root Required
736	#   chown / docker / etc. require root.
737	#
738	# Confirmation Required
739	#   Interactive mode prompts for YES. Use --force to bypass in scripts.
740	#
741	# ==============================================================================================
742	# RUNTIME MODES
743	# ==============================================================================================
744	#
745	# your_tool.sh
746	#     Normal run.
747	#
748	# your_tool.sh --dry-run
749	#     Preview without making changes.
750	#
751	# your_tool.sh --status
752	#     Show current state and exit.
753	#
754	# ==============================================================================================
755
756	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
757	source "$SCRIPT_DIR/../load_config.sh"
758	parse_args "$@"
759
760	if [[ "$EUID" -ne 0 ]]; then error "Must be run as root"; exit 1; fi
761
762	platform_require_cmd \
763	    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
764	    "" "" "unRAID notify script" || warn "notify not found — notifications disabled"
765
766	acquire_lock
767	detect_hosts
768
769	if [[ "$SHOW_STATUS" == true ]]; then
770	    log "Current state: ..."
771	    exit 0
772	fi
773
774	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
775
776	if [[ "$FORCE" != true ]]; then
777	    read -r -p "Type YES to proceed: " CONFIRM
778	    [[ "$CONFIRM" != "YES" ]] && { warn "Aborted."; exit 0; }
779	fi
780
781	# Do the work
782	# ...
783
784	notify "Tool completed on $(hostname) ($MY_ID)" "Tool Name" "normal"
785	```
786


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Ramdisk Stop ===============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Safely stops the transcode ramdisk: redirects the transcode symlink to the
9	# SSD fallback before unmounting so Emby continues writing without interruption,
10	# then unmounts the tmpfs and updates the state file.
11	#
12	# Primary use case: stopping the current ramdisk before re-running
13	# ramdisk_setup.sh with new size or threshold values (setup is idempotent —
14	# if the ramdisk is mounted, it skips the mount and reports status, so you
15	# must stop it first to change the size).
16	#
17	# ==============================================================================================
18	# OPERATIONAL MODEL
19	# ==============================================================================================
20	#
21	# Executes in safe order:
22	#   1. Validate — ramdisk mounted, SSD fallback exists
23	#   2. Redirect symlink → SSD (Emby immediately writes to SSD instead)
24	#   3. Warn if active transcode files still on ramdisk (informational — not a blocker)
25	#   4. Unmount ramdisk tmpfs
26	#   5. Update /tmp/transcode_state.db → current_target=TRANSCODE_SSD
27	#
28	# The symlink redirect happens before unmount so there is no window where Emby
29	# has nowhere to write. Existing in-progress transcode files on the ramdisk are
30	# lost on unmount — warn the user but proceed (this is expected for maintenance).
31	#
32	# ==============================================================================================
33	# OPERATIONAL SAFEGUARDS
34	# ==============================================================================================
35	#
36	# Root Required
37	#   umount requires root.
38	#
39	# Single Instance Lock
40	#   acquire_lock prevents concurrent stop attempts.
41	#
42	# Mounted Check
43	#   Exits cleanly if ramdisk is not mounted — nothing to do.
44	#
45	# Symlink-First Order
46	#   Symlink is redirected before unmount — Emby never sees a broken path.
47	#
48	# transcode_manager Warning
49	#   Warns if transcode_manager.sh is running — it may flip the symlink back
50	#   to ramdisk on its next cycle. Stop transcode_manager before running this
51	#   if you need the SSD redirect to hold.
52	#
53	# ==============================================================================================
54	# CONFIGURATION
55	# ==============================================================================================
56	#
57	# host*.conf
58	#   HOST*_TRANSCODE_SSD     SSD fallback directory — redirect target during stop.
59	#                           Aliased by detect_hosts() → TRANSCODE_SSD.
60	#
61	# master.conf
62	#   TRANSCODE_LINK          Symlink Emby uses. Must match Emby's transcode path setting.
63	#   RAMDISK_PATH            tmpfs mount point.
64	#   TRANSCODE_STATE_FILE    Override state file path (default: ${STATE_DIR}/transcode_state.db).
65	#
66	# ==============================================================================================
67	# STATE FILES
68	# ==============================================================================================
69	#
70	#   /tmp/transcode_state.db    — updated to current_target=TRANSCODE_SSD after stop.
71	#                                transcode_manager.sh reads this on its next cycle.
72	#
73	# ==============================================================================================
74	# RUNTIME MODES
75	# ==============================================================================================
76	#
77	# ramdisk_stop.sh
78	#     Stop the ramdisk: redirect symlink → SSD, unmount, update state.
79	#
80	# ramdisk_stop.sh --dry-run
81	#     Show what would happen without making any changes.
82	#
83	# ramdisk_stop.sh --status
84	#     Show current mount state, symlink target, active files on ramdisk. Exit.
85	#
86	# ramdisk_stop.sh --log
87	#     Verbose output for each step.
88	#
89	# ==============================================================================================
90
91	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
92
93	source "$SCRIPT_DIR/../load_config.sh"
94
95	parse_args "$@"
96
97	# ==============================================================================================
98	# ━━━ Setup ━━━
99	# ==============================================================================================
100	if [[ "$EUID" -ne 0 ]]; then


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= SMART Extended Self-Test =======================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Runs a SMART extended (long) self-test on all drives (or a specific drive),
9	# waits for completion, and reports results. Extended tests do a full read-scan
10	# of every sector — catches bad sectors that the short test skips. Monthly cadence
11	# via the monthly maintenance orchestrator.
12	#
13	# NVMe drives are included — smartctl supports NVMe self-test via the same
14	# -t long interface (NVMe 1.3+ specification).
15	#
16	# ==============================================================================================
17	# OPERATIONAL MODEL
18	# ==============================================================================================
19	#
20	# Starts long self-tests on all drives sequentially (NOT in parallel) to avoid
21	# saturating I/O. One drive at a time: start, wait for completion, move to next.
22	# Sequential order keeps test duration predictable and avoids thermal stacking.
23	#
24	# Drives already running a self-test are skipped. If a previous test did not
25	# finish (interrupted mid-way), it is reported and the drive is re-tested.
26	#
27	# Each test is polled every 60 seconds. Typical durations:
28	#   HDD  2–4 TB : 60–120 min
29	#   HDD  8–12 TB: 90–180 min
30	#   SSD  any    :  5–15 min
31	#   NVMe any    :  5–10 min
32	#
33	# ==============================================================================================
34	# OPERATIONAL SAFEGUARDS
35	# ==============================================================================================
36	#
37	# Single Instance Lock
38	#   acquire_lock prevents concurrent test runs — one long-test pass per server.
39	#
40	# Sequential Execution
41	#   Drives tested one at a time. Parallel long-tests thrash I/O and inflate temps.
42	#
43	# SIGTERM Trap
44	#   Poll loop exits cleanly on signal. Tests continue running in drive firmware.
45	#
46	# Tool Validation
47	#   platform_require_cmd confirms smartctl is present before use.
48	#
49	# Silent When Clean
50	#   Only failures and warnings produce notifications.
51	#
52	# ==============================================================================================
53	# CONFIGURATION
54	# ==============================================================================================
55	#
56	# host*.conf
57	#
58	#   HOST*_SMART_IGNORE_DRIVES
59	#       Drives skipped in SMART testing. Aliased by detect_hosts() →
60	#       SMART_IGNORE_DRIVES. Typically includes the boot USB flash drive.
61	#
62	# master.conf
63	#
64	# ==============================================================================================
65	# RUNTIME MODES
66	# ==============================================================================================
67	#
68	# smart_long_test.sh
69	#     Run extended self-test on all drives not in SMART_IGNORE_DRIVES.
70	#
71	# smart_long_test.sh /dev/sda
72	#     Run extended self-test on a specific drive. Bypasses the ignore list.
73	#
74	# smart_long_test.sh --status
75	#     Show last self-test result for all drives and exit.
76	#
77	# smart_long_test.sh --dry-run
78	#     Show which drives would be tested. No tests started.
79	#
80	# smart_long_test.sh --log
81	#     Verbose progress output during polling.
82	#
83	# ==============================================================================================
84
85	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
86
87	source "$SCRIPT_DIR/../load_config.sh"
88
89	parse_args "$@"
90
91	TARGET_DRIVE="${PARSED_ARGS[0]:-}"
92
93	# ==============================================================================================
94	# ━━━ Setup ━━━
95	# ==============================================================================================
96	if [[ "$EUID" -ne 0 ]]; then
97	    error "Must be run as root"
98	    exit 1
99	fi
100


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Claude Code Startup ========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Restores Claude Code's persistent data after an Unraid reboot.
9	# /root is RAM — wiped on every boot. This script re-creates symlinks so
10	# Claude's memory, sessions, settings, and binary survive across reboots.
11	#
12	# Storage mode is read from the host conf file:
13	#
14	#   HOST*_STORAGE_MODE_INTERNAL=true   → Internal (boot) mode
15	#       .claude data  → /boot/config/claude
16	#       binary        → /boot/config/claude-bin
17	#       No array dependency — runs even before array mounts.
18	#
19	#   HOST*_STORAGE_MODE_INTERNAL=false  → Appdata mode
20	#       .claude data  → /mnt/user/appdata/claude-code/.claude
21	#       binary        → /mnt/user/appdata/claude-code/local/share/claude
22	#       Requires array to be mounted.
23	#
24	# On first run in either mode, migrates any existing live data to persistent
25	# storage. Subsequent runs only re-create the symlinks.
26	#
27	# Standalone script — no common.sh dependency. Safe to run directly from
28	# terminal or from array_started.sh.
29	#
30	# ==============================================================================================
31	# RUNTIME MODES
32	# ==============================================================================================
33	#
34	# claude_startup.sh
35	#     Set up persistent symlinks only — default, used by array_started.sh on boot.
36	#
37	# claude_startup.sh --launch
38	#     Set up persistent symlinks and launch Claude interactively.
39	#
40	# ==============================================================================================
41
42	LAUNCH=false
43	[[ "$1" == "--launch" ]] && LAUNCH=true
44
45	_log()  { echo "  ✅  $*"; }
46	_warn() { echo "  ⚠   $*"; }
47	_err()  { echo "  ❌  $*" >&2; }
48
49	echo ""
50	echo "━━━ Claude Code Startup ━━━"
51	echo ""
52
53	# ── Detect storage mode ───────────────────────────────────────────────────────────────────────
54	CONF_DIR="/boot/config/plugins/varaverk/Configurations"
55	STORAGE_INTERNAL=false
56	for _conf in "$CONF_DIR"/host*.conf; do
57	    [[ -f "$_conf" ]] || continue
58	    if grep -q "_STORAGE_MODE_INTERNAL=true" "$_conf" 2>/dev/null; then
59	        STORAGE_INTERNAL=true
60	        break
61	    fi
62	done
63
64	if [[ "$STORAGE_INTERNAL" == true ]]; then
65	    CLAUDE_DATA="/boot/config/plugins/varaverk/claude-data"
66	    CLAUDE_BIN="/boot/config/plugins/varaverk/claude-bin"
67	    _log "Storage mode: internal boot"
68	else
69	    PERSIST_DIR="/mnt/user/appdata/claude-code"
70	    CLAUDE_DATA="$PERSIST_DIR/.claude"
71	    CLAUDE_BIN="$PERSIST_DIR/local/share/claude"
72	    _log "Storage mode: appdata"
73	fi
74
75	# ── Array check (appdata mode only) ──────────────────────────────────────────────────────────
76	if [[ "$STORAGE_INTERNAL" == false ]]; then
77	    if ! mountpoint -q /mnt/user 2>/dev/null; then
78	        _err "Array not mounted — /mnt/user not available (required for appdata mode)"
79	        exit 1
80	    fi
81	fi
82
83	# ── Create persistent dirs ────────────────────────────────────────────────────────────────────
84	mkdir -p "$CLAUDE_DATA" "$CLAUDE_BIN"
85
86	# ── Migrate from old /boot/config/claude location (internal mode only) ───────────────────────
87	if [[ "$STORAGE_INTERNAL" == true && -d /boot/config/claude && "$CLAUDE_DATA" != "/boot/config/claude" ]]; then
88	    if [[ -z "$(ls -A "$CLAUDE_DATA" 2>/dev/null)" ]]; then
89	        _warn "Migrating old /boot/config/claude → $CLAUDE_DATA"
90	        cp -a /boot/config/claude/. "$CLAUDE_DATA/"
91	        _log "Migrated .claude data from old location"
92	    fi
93	fi
94
95	# ── Migrate .claude on first run ──────────────────────────────────────────────────────────────
96	if [[ ! -L /root/.claude && -d /root/.claude ]]; then
97	    _warn "First run — migrating /root/.claude → $CLAUDE_DATA"
98	    cp -a /root/.claude/. "$CLAUDE_DATA/"
99	    rm -rf /root/.claude
100	    _log "Migrated .claude (memory, sessions, settings)"


Error: File does not exist. Note: your current working directory is /boot/config/plugins/varaverk.


The bandwidth_test.sh file doesn't exist. Let me search for all script files in the Tools directory to verify what scripts are actually present.


Bash

find /boot/config/plugins/varaverk/Tools -maxdepth 1 -type f -name "*.sh" | sort


Now I'll read the headers of the remaining scripts mentioned in the docs, and also check for scripts that exist but aren't documented (like docker_prune_images.sh).


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Docker Prune Images ============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Removes orphaned Docker images. Two modes:
9	#
10	# Default — dangling only (safe, fast):
11	#   Removes untagged images (no name, no container reference). These accumulate
12	#   after container updates pull a new image, leaving the old one untagged.
13	#   Running containers are never affected.
14	#
15	# --all — full orphan cleanup:
16	#   Step 1: removes stopped containers (exited/created state).
17	#   Step 2: removes all images not used by any running container.
18	#   Use this to clear tagged images left behind by removed or stopped apps.
19	#   CAUTION: also removes intentionally stopped containers — only run when you
20	#   know all stopped containers are safe to delete.
21	#
22	# ==============================================================================================
23	# RUNTIME MODES
24	# ==============================================================================================
25	#
26	# docker_prune_images.sh
27	#     Remove dangling (untagged) images only.
28	#
29	# docker_prune_images.sh --all
30	#     Remove stopped containers, then remove all unused images.
31	#
32	# docker_prune_images.sh --dry-run
33	#     Show what would be removed without making changes.
34	#
35	# docker_prune_images.sh --status
36	#     Show dangling images and stopped containers. No changes.
37	#
38	# ==============================================================================================
39
40	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
41
42	source "$SCRIPT_DIR/../load_config.sh"
43
44	# ── Handle --all before parse_args ────────────────────────────────────────────
45	ALL_MODE=false
46	FILTERED_ARGS=()
47	for _arg in "$@"; do
48	    [[ "$_arg" == "--all" ]] && ALL_MODE=true || FILTERED_ARGS+=("$_arg")
49	done
50	parse_args "${FILTERED_ARGS[@]}"
51
52	# ==============================================================================================
53	# ━━━ Setup ━━━
54	# ==============================================================================================
55	if [[ "$EUID" -ne 0 ]]; then
56	    error "Must be run as root"
57	    exit 1
58	fi
59
60	acquire_lock
61
62	# ==============================================================================================
63	# ━━━ Status ━━━
64	# ==============================================================================================
65	if [[ "$SHOW_STATUS" == true ]]; then
66	    echo ""
67	    echo "━━━━━ $ICON_SUMMARY DOCKER PRUNE IMAGES STATUS ━━━━━"
68
69	    DANGLING=$(docker images -f "dangling=true" --format "{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" 2>/dev/null)
70	    STOPPED=$(docker ps -a --filter "status=exited" --filter "status=created" \
71	        --format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null)
72
73	    echo ""
74	    echo "$ICON_CONTAINERS  Dangling images (no tag, no container):"
75	    if [[ -z "$DANGLING" ]]; then
76	        echo "    none"
77	    else
78	        echo "$DANGLING" | while IFS=$'\t' read -r id repo size age; do
79	            echo "    $id   $repo   $size   $age"
80	        done
81	    fi
82
83	    echo ""
84	    echo "$ICON_CONTAINERS  Stopped containers (--all would remove these first):"
85	    if [[ -z "$STOPPED" ]]; then
86	        echo "    none"
87	    else
88	        echo "$STOPPED" | while IFS=$'\t' read -r id name image status; do
89	            echo "    $name   ($image)   $status"
90	        done
91	    fi
92
93	    echo ""
94	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
95	    exit 0
96	fi
97
98	# ==============================================================================================
99	# ━━━ Run ━━━
100	# ==============================================================================================


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= fallback State Reset ===========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Resets the fallback state file to NORMAL and clears all tier flags. Use when
9	# the state file is stuck after fallback testing, a failed handback, manual
10	# intervention, or fallback.sh being killed mid-cycle.
11	#
12	# Writes a fresh state file with:
13	#   state=NORMAL / fallback_start=0 / handback_strikes=0
14	#   tier2_started=false / tier3_started=false / tier4_started=false
15	#
16	# Does NOT start or stop containers — state file only. After reset, fallback.sh
17	# resumes from NORMAL on its next cycle.
18	#
19	# WARNING: Only run when you have verified the stack is actually in a normal
20	# state — right containers on the right server, DDNS correct, no active fallback
21	# in progress. Resetting state during a real fallback causes fallback.sh to stop
22	# covering the remote server until the next detection cycle.
23	#
24	# ==============================================================================================
25	# OPERATIONAL SAFEGUARDS
26	# ==============================================================================================
27	#
28	# Active Fallback Detection
29	#   Checks whether fallback.sh is currently running and warns if so. A reset
30	#   during an active cycle causes fallback.sh to lose its state on the next read.
31	#
32	# Single Instance Lock
33	#   acquire_lock prevents concurrent resets.
34	#
35	# flock on State Write
36	#   The state file write is protected with flock — prevents a race condition
37	#   with fallback.sh reading the file mid-cycle.
38	#
39	# Confirmation Required
40	#   Interactive mode prompts for YES before writing. Use --force to bypass in
41	#   non-interactive contexts (cron, scripts).
42	#
43	# Notification Validated
44	#   platform_require_cmd confirms the notify script is present before use.
45	#
46	# ==============================================================================================
47	# RUNTIME MODES
48	# ==============================================================================================
49	#
50	# fallback_state_reset.sh
51	#     Show current state and prompt for YES before resetting.
52	#
53	# fallback_state_reset.sh --dry-run
54	#     Show current state and what the new state file would contain. No write.
55	#
56	# fallback_state_reset.sh --status
57	#     Show current state file contents and exit.
58	#
59	# fallback_state_reset.sh --force
60	#     Reset without interactive confirmation. Safe for scripted use.
61	#
62	# fallback_state_reset.sh --force --dry-run
63	#     Dry run without the confirmation prompt.
64	#
65	# ==============================================================================================
66
67	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
68
69	source "$SCRIPT_DIR/../load_config.sh"
70
71	# ── Handle --force flag before parse_args ─────────────────────────────────────────────────────
72	FORCE=false
73	FILTERED_ARGS=()
74	for arg in "$@"; do
75	    case "$arg" in
76	        --force) FORCE=true ;;
77	        *)       FILTERED_ARGS+=("$arg") ;;
78	    esac
79	done
80
81	parse_args "${FILTERED_ARGS[@]}"
82
83	# ==============================================================================================
84	# ━━━ Setup ━━━
85	# ==============================================================================================
86	if [[ "$EUID" -ne 0 ]]; then
87	    error "Must be run as root"
88	    exit 1
89	fi
90
91
92	acquire_lock
93
94	# detect_hosts() sets MY_ID — used in summary and notification
95	detect_hosts
96
97	log "$ICON_GEAR Config: state-file=${FALLBACK_STATE_FILE}"
98	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
99	[[ "$FORCE"   == true ]] && warn "FORCE mode — confirmation prompt skipped"
100


1	#!/bin/bash
2	# ==============================================================================================
3	# =========================== Watchdog Skip List Manager =======================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# View and manage the persistent container skip list used by docker_watchdog.sh.
9	# docker_watchdog.sh adds a container to the skip list when it exceeds
10	# WATCHDOG_CONTAINER_RESTART_LIMIT restarts within WATCHDOG_CONTAINER_RESTART_WINDOW
11	# hours — prevents infinite restart loops on containers that keep crashing.
12	#
13	# Skip list persists on /boot/config (survives reboots). Auto-clears when
14	# docker_watchdog.sh sees the container running on a later cycle. Use this
15	# script to clear manually after fixing the underlying problem.
16	#
17	# ==============================================================================================
18	# OPERATIONAL MODEL
19	# ==============================================================================================
20	#
21	# Skip List Lifecycle
22	#   1. Container crashes repeatedly → watchdog adds to skip list, notifies
23	#   2. Watchdog stops restarting the container on subsequent cycles
24	#   3a. If container recovers on its own (Docker restart policy), watchdog
25	#       sees it running, removes from skip list automatically
26	#   3b. If stuck stopped → fix the root cause, clear via this script, then
27	#       docker start ContainerName manually
28	#   4. Watchdog monitors normally on next cycle. If it crashes again → re-added.
29	#
30	# ==============================================================================================
31	# OPERATIONAL SAFEGUARDS
32	# ==============================================================================================
33	#
34	# Single Instance Lock
35	#   acquire_lock prevents concurrent access with docker_watchdog.sh writing
36	#   the same files.
37	#
38	# Active Watchdog Detection
39	#   Warns if docker_watchdog.sh is currently running when a clear is attempted
40	#   — the watchdog could re-add the container to the skip list within seconds.
41	#
42	# Docker Timeout
43	#   DOCKER_TIMEOUT caps docker inspect calls against a hung daemon.
44	#
45	# Confirmation Required
46	#   Interactive mode prompts for YES before clearing. Use --force for scripts.
47	#
48	# ==============================================================================================
49	# STATE FILES
50	# ==============================================================================================
51	#
52	#   DOCKER_WATCHDOG_FAILED_FILE        — persistent container skip list (on /boot/config)
53	#   WATCHDOG_CONTAINER_RESTART_LOG  — restart history used for loop detection
54	#
55	# ==============================================================================================
56	# RUNTIME MODES
57	# ==============================================================================================
58	#
59	# watchdog_skip_list_manager.sh [--status]
60	#     Show skip list, container states, and recent restart history.
61	#
62	# watchdog_skip_list_manager.sh --clear ContainerName
63	#     Remove a specific container from the skip list and clear its restart history.
64	#     Prompts for YES unless --force is passed.
65	#
66	# watchdog_skip_list_manager.sh --clear-all
67	#     Clear all skip lists and all restart history.
68	#     Prompts for YES unless --force is passed.
69	#
70	# All actions support --dry-run (show what would change) and --force (skip prompt).
71	#
72	# ==============================================================================================
73
74	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
75
76	source "$SCRIPT_DIR/../load_config.sh"
77
78	DOCKER_TIMEOUT=15
79
80	# ── Parse action flags before parse_args ──────────────────────────────────────────────────────
81	ACTION="status"
82	TARGET_CONTAINER=""
83	FORCE=false
84	FILTERED_ARGS=()
85
86	for arg in "$@"; do
87	    case "$arg" in
88	        --clear-all)  ACTION="clear-all" ;;
89	        --clear)      ACTION="clear" ;;
90	        --status)     ACTION="status" ;;
91	        --force)      FORCE=true ;;
92	        *)
93	            if [[ "$ACTION" == "clear" && -z "$TARGET_CONTAINER" ]]; then
94	                TARGET_CONTAINER="$arg"
95	            else
96	                FILTERED_ARGS+=("$arg")
97	            fi
98	            ;;
99	    esac
100	done


Now let me analyze the findings. Let me check the claude_startup.sh documentation more carefully, since I see a potential discrepancy.


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Claude Code Startup ========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Restores Claude Code's persistent data after an Unraid reboot.
9	# /root is RAM — wiped on every boot. This script re-creates symlinks so
10	# Claude's memory, sessions, settings, and binary survive across reboots.
11	#
12	# Storage mode is read from the host conf file:
13	#
14	#   HOST*_STORAGE_MODE_INTERNAL=true   → Internal (boot) mode
15	#       .claude data  → /boot/config/claude
16	#       binary        → /boot/config/claude-bin
17	#       No array dependency — runs even before array mounts.
18	#
19	#   HOST*_STORAGE_MODE_INTERNAL=false  → Appdata mode
20	#       .claude data  → /mnt/user/appdata/claude-code/.claude
21	#       binary        → /mnt/user/appdata/claude-code/local/share/claude
22	#       Requires array to be mounted.
23	#
24	# On first run in either mode, migrates any existing live data to persistent
25	# storage. Subsequent runs only re-create the symlinks.
26	#
27	# Standalone script — no common.sh dependency. Safe to run directly from
28	# terminal or from array_started.sh.
29	#
30	# ==============================================================================================
31	# RUNTIME MODES
32	# ==============================================================================================
33	#
34	# claude_startup.sh
35	#     Set up persistent symlinks only — default, used by array_started.sh on boot.
36	#
37	# claude_startup.sh --launch
38	#     Set up persistent symlinks and launch Claude interactively.
39	#
40	# ==============================================================================================
41
42	LAUNCH=false
43	[[ "$1" == "--launch" ]] && LAUNCH=true
44
45	_log()  { echo "  ✅  $*"; }


Now I've gathered all the information needed. Let me compile the discrepancies:

## REPORT: Documentation vs Script Header Discrepancies

**1. claude_startup.sh — Runtime Mode Documentation**

**README-Tools.md says:** "After each unRAID reboot, or called by array_started.sh"

**Manual-Tools.md says:**
- `claude_startup.sh` → "set up persistent symlinks only — default, used by array_started.sh on boot"
- `claude_startup.sh --launch` → "Set up persistent symlinks and launch Claude interactively"

**Script header says:**
- `claude_startup.sh` → "Set up persistent symlinks only — default, used by array_started.sh on boot"
- `claude_startup.sh --launch` → "Set up persistent symlinks and launch Claude interactively"

**Issue:** Manual documents `--setup` flag (in the "Calling from array_started.sh" section) but the script header and actual code only support `--launch`. The manual states: "To auto-restore Claude data on every boot without launching an interactive session: `/path/to/Tools/claude_startup.sh --setup`" but the script only recognizes `--launch`. Default behavior (no flag) sets up symlinks; `--launch` adds the interactive launch.

**2. fallback_state_reset.sh — Built Status**

**Manual-Tools.md line 14 says:** "*(not yet built — manual workaround)*"

**Script header (lines 1-25) clearly shows:** A fully functional script with PURPOSE, OPERATIONAL SAFEGUARDS, and RUNTIME MODES documented.

**Issue:** The manual outdated — script is built and complete, not "not yet built."

**3. docker_prune_images.sh — Missing from Documentation**

**Script exists:** `/boot/config/plugins/varaverk/Tools/docker_prune_images.sh` is fully implemented with PURPOSE, RUNTIME MODES.

**README-Tools.md and Manual-Tools.md:** No mention of this script anywhere in the scripts table or contents.

**Issue:** `docker_prune_images.sh` exists and is functional but is not documented.

**4. bandwidth_test.sh — File Not Found**

**README-Tools.md line 128 references:** "`ramdisk_stop.sh`" (not bandwidth_test.sh)

**Manual-Tools.md references:** "`ramdisk_stop.sh`" (not bandwidth_test.sh)

**Requested file:** `bandwidth_test.sh` does not exist in Tools/ directory.

**Issue:** No discrepancy here — the requested file doesn't exist and the documentation correctly references `ramdisk_stop.sh`.