Skill: pi-config

Patterns for Raspberry Pi boot partition configuration injection.

7 stars

Best use case

Skill: pi-config is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Patterns for Raspberry Pi boot partition configuration injection.

Teams using Skill: pi-config should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/pi-config/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/hardware-iot/pi-provisioner/skills/pi-config/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/pi-config/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Skill: pi-config Compares

Feature / AgentSkill: pi-configStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Patterns for Raspberry Pi boot partition configuration injection.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Skill: pi-config

Patterns for Raspberry Pi boot partition configuration injection.

## Boot partition layout

After `dd` writes the image, the first partition on the SD card is the boot partition (FAT32, label `bootfs` or `boot`).
This partition contains files read by the Pi firmware and the first-boot service (`pi-init`).

| File | Purpose |
|------|---------|
| `ssh` | Empty file - enables SSH daemon on first boot |
| `wpa_supplicant.conf` | Wi-Fi config (WPA2-Personal) - read on first boot and removed |
| `userconf` | `username:sha512crypt_hash` - creates initial user on first boot |
| `config.txt` | Hardware config: dtoverlay, GPU memory, HDMI settings |
| `cmdline.txt` | Kernel command line (do not overwrite; append only) |

## Mounting the boot partition

### Linux

```ts
import { execa } from 'execa';
import path from 'path';
import fs from 'fs/promises';
import os from 'os';

export async function mountBootPartition(device: string): Promise<string> {
  // e.g. /dev/sdb -> /dev/sdb1
  const partition = `${device}1`;
  const mountPoint = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-boot-'));

  await execa('mount', [partition, mountPoint]);
  return mountPoint;
}

export async function unmount(mountPoint: string): Promise<void> {
  await execa('umount', [mountPoint]);
  await fs.rm(mountPoint, { recursive: true, force: true });
}
```

### macOS

On macOS, after `dd` the boot partition is auto-mounted at `/Volumes/bootfs`. No manual mount needed.
Detect the mount point by checking `/Volumes`:

```ts
export async function findMacOsBootMount(device: string): Promise<string> {
  // disk2 -> disk2s1
  const partition = device.replace('/dev/', '') + 's1';
  const { stdout } = await execa('diskutil', ['info', '-plist', partition]);
  const plist = parsePlist(stdout);  // use 'plist' npm package
  return plist.MountPoint as string;
}
```

## wpa_supplicant.conf

```ts
export function buildWpaSupplicant(ssid: string, password: string, country: string): string {
  return [
    'ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev',
    'update_config=1',
    `country=${country.toUpperCase()}`,
    '',
    'network={',
    `\tssid="${ssid}"`,
    `\tpsk="${password}"`,
    '\tkey_mgmt=WPA-PSK',
    '}',
  ].join('\n') + '\n';
}
```

Write to `<mountPoint>/wpa_supplicant.conf`.

Note: As of Raspberry Pi OS Bookworm (2023-10), the OS no longer reads `wpa_supplicant.conf` from the boot partition during cloud-init. Instead use `custom.toml` (see below). For compatibility with older images, write both.

## cloud-init (Pi OS Bookworm and later)

Newer Pi OS images use cloud-init. Write `user-data` and `network-config` to the boot partition.

```ts
export function buildUserData(hostname: string, username: string, passwordHash: string, enableSsh: boolean): string {
  const lines = [
    '#cloud-config',
    '',
    `hostname: ${hostname}`,
    'manage_etc_hosts: true',
    '',
    'users:',
    `  - name: ${username}`,
    `    password: ${passwordHash}`,
    '    chpasswd:',
    '      expire: false',
    '    shell: /bin/bash',
    '    sudo: ALL=(ALL) NOPASSWD:ALL',
    '    lock_passwd: false',
  ];

  if (enableSsh) {
    lines.push('', 'ssh_pwauth: true');
  }

  return lines.join('\n') + '\n';
}

export function buildNetworkConfig(ssid: string, password: string): string {
  return [
    'version: 2',
    'wifis:',
    '  wlan0:',
    '    dhcp4: true',
    '    optional: true',
    '    access-points:',
    `      "${ssid}":`,
    `        password: "${password}"`,
  ].join('\n') + '\n';
}
```

## Full config injection

```ts
// packages/server/src/provision/config-injector.ts
import path from 'path';
import fs from 'fs/promises';
import { buildWpaSupplicant, buildUserData, buildNetworkConfig } from './templates';
import { hashPassword } from './crypto';

export async function injectConfig(mountPoint: string, config: ProvisionConfig): Promise<void> {
  const write = (filename: string, content: string) =>
    fs.writeFile(path.join(mountPoint, filename), content, 'utf8');

  // Legacy SSH enable (Pi OS Buster/Bullseye)
  if (config.enable_ssh) {
    await write('ssh', '');
  }

  // Legacy user (Pi OS Bullseye and earlier)
  if (config.username && config.password_hash) {
    await write('userconf', `${config.username}:${config.password_hash}`);
  }

  // Legacy Wi-Fi (Pi OS Bullseye and earlier)
  if (config.wifi_ssid && config.wifi_password) {
    await write(
      'wpa_supplicant.conf',
      buildWpaSupplicant(config.wifi_ssid, config.wifi_password, config.wifi_country ?? 'GB')
    );
  }

  // Cloud-init (Pi OS Bookworm)
  if (config.username && config.password_hash) {
    await write(
      'user-data',
      buildUserData(config.hostname, config.username, config.password_hash, config.enable_ssh)
    );
  }

  if (config.wifi_ssid && config.wifi_password) {
    await write(
      'network-config',
      buildNetworkConfig(config.wifi_ssid, config.wifi_password)
    );
  }

  // Ensure writes are flushed to disk before unmounting
  await execa('sync');
}
```

## config.txt modifications

Append only - never replace the entire config.txt:

```ts
export async function appendConfigTxt(mountPoint: string, lines: string[]): Promise<void> {
  const configPath = path.join(mountPoint, 'config.txt');
  const existing = await fs.readFile(configPath, 'utf8').catch(() => '');
  const toAppend = lines.filter((l) => !existing.includes(l));
  if (toAppend.length > 0) {
    await fs.appendFile(configPath, '\n# pi-provisioner additions\n' + toAppend.join('\n') + '\n');
  }
}
```

Common config.txt additions:
- `dtparam=audio=on` - enable audio
- `gpu_mem=16` - minimal GPU memory for headless
- `dtoverlay=disable-wifi` - disable Wi-Fi

## Simulate mode boot partition

When `ALLOW_SIMULATED_WRITE=1`, write config files to a temporary directory instead of a real mount:

```ts
export async function getSimulatedBootMount(): Promise<string> {
  const dir = path.join(os.tmpdir(), 'pi-provisioner-sim-boot');
  await fs.mkdir(dir, { recursive: true });
  return dir;
}
```

The config files written to this temp dir can be logged for debugging.

## cmdline.txt - hostname approach (older images)

Some older images set the hostname via cmdline.txt. Avoid this - use `userconf` or cloud-init `hostname:` instead. Never modify `cmdline.txt` hostname unless you are certain the image reads it.

## Sync before unmount

Always run `sync` before unmounting to ensure filesystem buffers are flushed:

```ts
await execa('sync');
await execa('umount', [mountPoint]);
```

On macOS use `diskutil eject`:

```ts
await execa('diskutil', ['eject', partition]);
```

## Pi OS version detection heuristic

Check for `user-data` in the boot partition to determine if it is a Bookworm+ image:

```ts
async function isBookwormOrLater(mountPoint: string): Promise<boolean> {
  try {
    await fs.access(path.join(mountPoint, 'user-data'));
    return true;
  } catch {
    return false;
  }
}
```

Use this to decide whether to write cloud-init files vs legacy `userconf`/`wpa_supplicant.conf`.

## Wi-Fi password storage rule

The Wi-Fi password must never be stored in SQLite. Pass it through in memory from the wizard form body directly to `injectConfig`, then discard. The `provisions` table schema does not have a `wifi_password` column.

Related Skills

nginx-config-generator

7
from heldernoid/agentic-build-templates

Generate production-ready nginx configuration files for reverse proxy, SSL, rate limiting, and caching setups. Use when you need an nginx config for a web application, API, static site, or reverse proxy. Triggers include "nginx config", "nginx configuration", "nginx setup", "reverse proxy config", "SSL nginx", "nginx rate limiting", or any request involving nginx web server configuration.

dns-config

7
from heldernoid/agentic-build-templates

Manage the local-dns-override hosts.yaml configuration file. Use when you need to view the current DNS config, validate a hosts.yaml file before applying it, understand the config file format, or check which records are currently loaded. Triggers include "show DNS config", "validate hosts.yaml", "check loaded records", "DNS config file", "list local DNS entries", or any task involving viewing or validating the DNS record configuration.

validate-config

7
from heldernoid/agentic-build-templates

Validate a config file (JSON, YAML, TOML, or .env) against a JSON Schema using config-validator. Use when you need to check that a config file is valid before deploying, catch missing required fields or wrong types, or run validation in a CI pipeline. Triggers include "validate config", "check config file", "schema validation", "validate yaml", "validate json config", "config has errors", or any task involving checking whether a config file matches its expected schema.

ssh-config-manager

7
from heldernoid/agentic-build-templates

Manage SSH host configurations in ~/.ssh/config from the terminal. Use when adding, editing, or searching SSH hosts, cloning host configs, testing connections, or importing configs. Triggers include "ssh config", "ssh host", "sshm", "add ssh host", "edit ssh config", "test ssh connection".

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.