Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ jobs:
- run: npm install -g @anthropic-ai/claude-code
- run: claude plugin validate .
- run: for p in plugins/*/; do claude plugin validate "$p" || exit 1; done
# The worked example in docs/mods.md.
- run: claude plugin validate docs/examples/homelab-guard
- run: claude plugin test docs/examples/homelab-guard
93 changes: 76 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# claude

Claude Code plugins for running a homelab, published as one marketplace named `jonasthim`.
Claude Code plugins for running a homelab, published as one marketplace named `jonasthim`. Talk to Claude
about your NAS, hypervisor or network and it inspects, troubleshoots and changes them through their official
APIs. It asks before anything destructive.

| Plugin | What it operates | Contents |
|---|---|---|
| [`truenas`](plugins/truenas) | TrueNAS SCALE 25.x: pools, datasets, snapshots, shares, apps, alerts, updates, replication | skill `truenas`, `tn.py` (JSON-RPC websocket or SSH `midclt`) |
| [`proxmox`](plugins/proxmox) | Proxmox VE 9: VMs, containers, snapshots, backups, storage, networking/SDN, HA, access control | skill `pve`, `/proxmox:*` commands, `proxmox-operator` agent, destructive-action guard hook |
| [`unifi`](plugins/unifi) | UniFi networks: client troubleshooting, health reports, guarded config changes, device actions | skill `unifi-ops`, `unifi.py` (Network Integration and Site Manager APIs), SSH diagnostics |
| Plugin | What it operates | You need | Contents |
|---|---|---|---|
| [`truenas`](plugins/truenas) | TrueNAS SCALE 25.x: pools, datasets, snapshots, shares, apps, alerts, updates, replication | API key, or SSH to the NAS | skill `truenas`, `tn.py` |
| [`proxmox`](plugins/proxmox) | Proxmox VE 9: VMs, containers, snapshots, backups, storage, networking/SDN, HA, access control | API token; SSH optional | skill `pve`, `/proxmox:*` commands, `proxmox-operator` agent, guard hook |
| [`unifi`](plugins/unifi) | UniFi networks: client troubleshooting, health reports, networks/VLANs, SSIDs, firewall, device actions | Integration API key; Site Manager key and SSH optional | skill `unifi-ops`, `unifi.py`, SSH diagnostics |

Each plugin installs on its own, so you only load what you run. **Full documentation: [docs/](docs/index.md).**

Each plugin installs on its own, so you only load what you run.
## Quick start

## Install
**1. Install**

```
claude plugin marketplace add jonasthim/claude
Expand All @@ -19,18 +23,71 @@ claude plugin install proxmox@jonasthim
claude plugin install unifi@jonasthim
```

or from inside a session: `/plugin marketplace add jonasthim/claude`, then `/plugin install <plugin>@jonasthim`.
Or inside a session: `/plugin marketplace add jonasthim/claude`, then `/plugin install <plugin>@jonasthim`. If
the repository is private and the add fails, clone it (`gh repo clone jonasthim/claude ~/src/claude`) and
run `claude plugin marketplace add ~/src/claude`.

**2. Configure**: export credentials in the shell that starts Claude Code, ideally from a file only you can read:

```
# ~/.config/homelab.env (chmod 600), then: source ~/.config/homelab.env && claude
export TRUENAS_HOST=nas.example.lan TRUENAS_API_KEY='1-...'
export PVE_HOST=pve1.example.lan PVE_TOKEN_ID='claude@pve!ro' PVE_TOKEN_SECRET='...'
export PVE_CA_CERT=~/.config/pve-root-ca.pem
export UNIFI_HOST=192.168.1.1 UNIFI_API_KEY='...'
```

| Plugin | Where to create the credential | Read-only to start with |
|---|---|---|
| truenas | Web UI → your user icon → *API Keys* → *Add* | Key of a `READONLY_ADMIN` user |
| proxmox | On a node: `pveum user token add ...`, or *Datacenter → Permissions → API Tokens*. You create it, not Claude: the secret is shown once | `PVEAuditor` on `/` |
| unifi | Network app → *Settings → Control Plane → Integrations → API Keys* | Key of a read-only admin |

Every variable is listed on the plugin pages: [TrueNAS](docs/truenas.md), [Proxmox VE](docs/proxmox.md),
[UniFi](docs/unifi.md).

If the repository is private, the marketplace add needs GitHub access that can read it (`gh auth login`,
a credential helper or an SSH key). Otherwise clone it and add the local path:
**3. Check the setup**: start a new session and ask "is my NAS healthy?", run `/proxmox:doctor`, or ask
"check my unifi setup".

## What to ask

| TrueNAS | Proxmox VE | UniFi |
|---|---|---|
| "Is my NAS healthy? Anything I should worry about?" | `/proxmox:status` for a cluster overview | "Why does my laptop keep dropping off wifi?" |
| "Snapshot tank/photos, then make the photos SMB share read-only for guests." | "Snapshot 101 before I upgrade it." | "Give me a health report." |
| "Jellyfin is stuck deploying, figure out why and fix it." | "Why did last night's backup of 105 fail?" | "Create a guest network on VLAN 30 with its own SSID." |
| "Which snapshots on tank/backup are older than 60 days?" | `/proxmox:vm`, `/proxmox:ct`, `/proxmox:snapshot`, `/proxmox:backup` | "Restart the hallway AP." |

## Safety model

- **Reads run freely.**
- **Destructive or disruptive actions need your yes.** Claude shows a plan first and waits for your answer.
A code-level gate backs this up: `tn.py --confirm`, `unifi.py --yes` (with `--dry-run` to preview), and
for Proxmox a guard hook that prompts even in auto mode.
- **Secrets stay out of the chat.** Claude never asks for or prints credentials, redacts them in output, and
never turns TLS verification off on its own.
- **Least privilege** is your last line: a read-only key or token cannot do damage.

The details and a per-plugin comparison are in [docs/safety.md](docs/safety.md).

## Update and uninstall

```
gh repo clone jonasthim/claude ~/src/claude
claude plugin marketplace add ~/src/claude
claude plugin marketplace update jonasthim
claude plugin update <plugin>@jonasthim
claude plugin uninstall <plugin>@jonasthim
```

Update later with `claude plugin marketplace update jonasthim` and `claude plugin update <plugin>@jonasthim`.
Each plugin's README covers its configuration (environment variables, API keys, SSH) and safety model.
## Documentation

| Page | |
|---|---|
| [Getting started](docs/getting-started.md) | Install, configure, first checks, moving from the old repositories |
| [TrueNAS](docs/truenas.md), [Proxmox VE](docs/proxmox.md), [UniFi](docs/unifi.md) | Credentials, variables, commands, safety and troubleshooting per plugin |
| [Safety model](docs/safety.md) | What runs on its own, what asks first, secrets and TLS |
| [Building a mod](docs/mods.md) | Function-hook plugins, with a worked, tested example |

Each plugin's README under [`plugins/`](plugins) is its full reference.

## Develop

Expand All @@ -42,12 +99,14 @@ bash plugins/proxmox/tests/run.sh # pro
bash plugins/unifi/tests/smoke.sh # unifi (offline, against fixtures)
```

UniFi also runs without a controller: `export UNIFI_MOCK_DIR=$PWD/plugins/unifi/skills/unifi-ops/evals/fixtures`.
Shared conventions are in [CONTRIBUTING.md](CONTRIBUTING.md).

## Mods

The marketplace can also carry mods: Claude Code plugins of function hooks that add a pane, a band
above the prompt, a status line entry or a tool-call hook. None ship yet; see [mods/README.md](mods/README.md).
The marketplace can also carry mods: plugins made of function hooks that intercept tool calls, decide
permissions, or add a status line entry, a band or a pane. None ship yet. [docs/mods.md](docs/mods.md) walks
through building one.

## License

Expand Down
5 changes: 5 additions & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
title: jonasthim homelab plugins for Claude Code
description: TrueNAS, Proxmox VE and UniFi plugins for Claude Code, from one marketplace.
theme: jekyll-theme-primer
exclude:
- examples
6 changes: 6 additions & 0 deletions docs/examples/homelab-guard/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "homelab-guard",
"version": "0.1.0",
"author": { "name": "Jonas Thim" },
"description": "Asks before any confirmed destructive TrueNAS, Proxmox or UniFi call runs."
}
34 changes: 34 additions & 0 deletions docs/examples/homelab-guard/hooks/guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from 'claude-code/testing'

const check = (command: string) => ({ tool: 'Bash', input: { command } })

describe('homelab-guard', () => {
test('asks before a confirmed TrueNAS call', async ($, on) => {
on('tool.check', () => ({ decision: 'allow' }))
const verdict = await $.tool.check(check('python3 tn.py call pool.dataset.delete \'["tank/x"]\' --confirm'))
expect(verdict.decision).toBe('ask')
})

test('asks before a UniFi write and a lowercase Proxmox delete', async ($, on) => {
on('tool.check', () => ({ decision: 'allow' }))
expect((await $.tool.check(check('python3 unifi.py devices restart d1 --yes'))).decision).toBe('ask')
expect((await $.tool.check(check('pve-api.sh delete /nodes/pve1/qemu/100'))).decision).toBe('ask')
})

test('asks when the command is split across lines', async ($, on) => {
on('tool.check', () => ({ decision: 'allow' }))
expect((await $.tool.check(check('python3 tn.py call pool.dataset.delete \\\n \'["tank/x"]\' --confirm'))).decision).toBe('ask')
expect((await $.tool.check(check('python3 unifi.py wifi delete w1 \\\n --yes'))).decision).toBe('ask')
})

test('leaves reads and dry runs alone', async ($, on) => {
on('tool.check', () => ({ decision: 'allow' }))
expect((await $.tool.check(check('python3 tn.py call pool.query'))).decision).toBe('allow')
expect((await $.tool.check(check('python3 unifi.py devices restart d1 --dry-run'))).decision).toBe('allow')
})

test('never loosens a deny', async ($, on) => {
on('tool.check', () => ({ decision: 'deny', reason: 'blocked by a rule' }))
expect((await $.tool.check(check('python3 unifi.py wifi delete w1 --yes'))).decision).toBe('deny')
})
})
1 change: 1 addition & 0 deletions docs/examples/homelab-guard/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "modules": ["./register.ts"] }
28 changes: 28 additions & 0 deletions docs/examples/homelab-guard/hooks/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Register } from 'claude-code'

// Commands that carry a plugin's own "I have confirmation" flag, or an HTTP DELETE.
// Each entry: a pattern over the Bash command, then the reason the dialog shows.
// The `s` flag lets `.` cross newlines, so a command split with `\` is matched too.
const GATED: readonly [RegExp, string][] = [
[/\btn\.py\b.*\s--confirm\b/s, 'a gated TrueNAS call (tn.py --confirm)'],
[/\bunifi\.py\b.*\s--yes\b/s, 'a UniFi write (unifi.py --yes)'],
[/\bpve-api\.sh\s+delete\b/i, 'an HTTP DELETE against Proxmox (pve-api.sh DELETE)'],
]

export function gatedReason(command: string): string | undefined {
for (const [pattern, reason] of GATED) {
if (pattern.test(command)) return reason
}
return undefined
}

export const register: Register = on => {
on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
const verdict = await next(e)
const command = (e.input as { command?: unknown }).command
const reason = typeof command === 'string' ? gatedReason(command) : undefined
// Never loosen a deny; turn an allow into an ask for gated commands.
if (reason === undefined || verdict.decision === 'deny') return verdict
return { decision: 'ask', reason: `${$.plugin.name}: ${reason}. Confirm it is what you agreed to.` }
}).catch(() => ({ decision: 'ask', reason: 'homelab-guard failed; confirm this command yourself.' }))
}
73 changes: 73 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Getting started

## 1. Install

From a terminal:

```
claude plugin marketplace add jonasthim/claude
claude plugin install truenas@jonasthim
claude plugin install proxmox@jonasthim
claude plugin install unifi@jonasthim
```

Or inside a Claude Code session: `/plugin marketplace add jonasthim/claude`, then
`/plugin install <plugin>@jonasthim`. Install only the plugins for systems you run.

If the repository is private, the marketplace add needs GitHub access that can read it (`gh auth login`, a
credential helper or an SSH key). Otherwise clone it and add the local path:

```
gh repo clone jonasthim/claude ~/src/claude
claude plugin marketplace add ~/src/claude
```

A plugin installed during a session becomes active in the next session (or after `/reload-plugins` where your
version has it).

## 2. Configure

Every plugin reads its connection settings from environment variables in the shell that starts Claude Code.
Keep them in a file only you can read and source it, so secrets never go into a prompt or a committed file:

```
# ~/.config/homelab.env (chmod 600)
export TRUENAS_HOST=nas.example.lan
export TRUENAS_API_KEY='1-...'

export PVE_HOST=pve1.example.lan
export PVE_TOKEN_ID='claude@pve!ro'
export PVE_TOKEN_SECRET='...'
export PVE_CA_CERT=~/.config/pve-root-ca.pem

export UNIFI_HOST=192.168.1.1
export UNIFI_API_KEY='...'
```

```
source ~/.config/homelab.env && claude
```

Start with read-only credentials and widen them when you need Claude to make changes. Where to create each
key, and every optional variable, is on the plugin pages: [TrueNAS](truenas.md), [Proxmox VE](proxmox.md),
[UniFi](unifi.md).

## 3. First checks

| Plugin | Ask or run | You should see |
|---|---|---|
| truenas | "Is my NAS healthy?" | Pool, alert and capacity summary |
| proxmox | `/proxmox:doctor` | A `check / result / detail` table and what the token can do per ACL path |
| unifi | "check my unifi setup" | The application version and site, or what is missing |

## 4. Update and uninstall

```
claude plugin marketplace update jonasthim
claude plugin update <plugin>@jonasthim
claude plugin uninstall <plugin>@jonasthim
```

Moving from the old single-plugin repositories (`claude-truenas-skill`, `claude-proxmox-skill`,
`claude-unifi-skill`)? Uninstall the plugins installed from those marketplaces, remove the marketplaces, and
install from `jonasthim` as above.
25 changes: 25 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Homelab plugins for Claude Code

Three [Claude Code](https://claude.com/claude-code) plugins for running a homelab, published from one
marketplace named `jonasthim`. Each plugin installs on its own.

| Plugin | Operates | Needs |
|---|---|---|
| [truenas](truenas.md) | TrueNAS SCALE 25.x: pools, datasets, snapshots, shares, apps, alerts, updates, replication | API key, or SSH to the NAS |
| [proxmox](proxmox.md) | Proxmox VE 9: VMs, containers, snapshots, backups, storage, networking/SDN, HA, access control | API token (SSH optional) |
| [unifi](unifi.md) | UniFi networks: clients, devices, health, networks/VLANs, SSIDs, firewall | Integration API key (Site Manager key and SSH optional) |

```
claude plugin marketplace add jonasthim/claude
claude plugin install truenas@jonasthim # and/or proxmox, unifi
```

## Pages

- [Getting started](getting-started.md): install, configure, first checks, update and uninstall
- [TrueNAS](truenas.md), [Proxmox VE](proxmox.md), [UniFi](unifi.md): setup and use per plugin
- [Safety model](safety.md): what Claude does on its own, what it asks first, and how secrets are handled
- [Building a mod](mods.md): add your own function-hook plugin to this marketplace, with a worked example

Each plugin's own README in [`plugins/`](https://github.com/jonasthim/claude/tree/main/plugins) is the full
reference; these pages summarise it.
Loading
Loading