Package a "claw"-type local-first AI agent (openclaw, nullclaw, zeroclaw, hermes-agent, picoclaw, odysseus, and future siblings) as a classic-confinement snap, consistently with the existing collection in ~/src/github/ai-labs/snaps. Use when creating a new claw snap, fixing build/release/CI workflows for one, or making the collection more consistent — covers snapcraft.yaml structure, version adoption per runtime, bundling Node/Chromium, self-contained Python venvs on core24, why not to patchelf interpreters under classic confinement, the systemd user-service pattern, and the PR/release/check-releases GitHub workflows.
72
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
The ~/src/github/ai-labs/snaps collection packages local-first AI agents as
classic-confinement snaps (they run arbitrary user code, browse files, exec
tools — strict confinement can't express that). openclaw is the reference; copy
its structure. For the bundled local-model picker, see the claw-lemonade-tool skill.
<name>-snap/
snap/snapcraft.yaml
snap/local/bin/<name>-launch # CLI wrapper: sets env, installs service, exec app
snap/local/bin/<name>-daemon-launch # gateway daemon wrapper (if the agent has one)
snap/local/bin/<name>-lemonade(-launch) # lemonade model picker (see claw-lemonade-tool)
snap/local/bin/setup-providers(.js) # auto/interactive provider config
snap/local/share/systemd/user/<name>.service
snap/local/share/completions/<name>.bash # optional bash completion (completer:)
.github/workflows/{pr,release,check-releases}.yaml
package.json # npm-runtime snaps: version placeholder for check-releases
.gitignore # *.snap parts/ stage/ prime/ .snapcraft/
README.mdbase: core24, confinement: classic, grade: stable, adopt-info: <name>.launcher part (plugin: dump, source: snap/local) with an organize: map
listing every script/unit/completion. The app command:s point at bin/<name>-launch.craftctl set version=... — method depends on runtime.plugin: npm, npm-include-node: true,
npm-node-version. In override-build: craftctl default, then
npm install -g --prefix "$CRAFT_PART_INSTALL" <pkg>, then read the installed
package.json version via the bundled node. check-releases.yaml polls
registry.npmjs.org/<pkg> for ['dist-tags'].latest.
⚠️ Use JSON.parse(d)['dist-tags'].latest — JSON.parse(d).['dist-tags'] is a
silent syntax error that breaks the auto-update workflow (was a real bug in openclaw).plugin: nil, override-pull curls
api.github.com/repos/<o>/<r>/releases/latest for tag_name, downloads the
per-arch asset, craftctl set version. Map $CRAFT_ARCH_BUILD_FOR→upstream arch name.plugin: nil + a manual venv (NOT plugin: python —
see "Self-contained Python" below); pip install <pkg>; read version from the
installed *.dist-info/METADATA. check-releases.yaml polls PyPI.plugin: nil + source: git; build a venv (see
"Self-contained Python"), copy app source to $CRAFT_PART_INSTALL/opt/<name>,
and have the launcher copy it to a writable $SNAP_USER_COMMON dir on first run /
refresh (the app writes next to __file__).--no-sandbox.
Bake it at build time: find the chrome binary, mv it to chrome-real, drop a
wrapper exec .../chrome-real --no-sandbox --disable-setuid-sandbox "$@". Set
PLAYWRIGHT_BROWSERS_PATH=$SNAP/usr/share/ms-playwright in each app's environment:.node/python) to core24's linker.
Classic confinement runs against the host dynamic linker, and a core24-built
binary is forward-compatible with a newer host glibc. Patching the interpreter to
/snap/core24/current/... instead segfaults silently on hosts whose core24/glibc
combination differs (observed on Ubuntu 26.04: ldd and the binary both produce no
output, exit 139). openclaw deliberately ships its bundled node un-patched — do the
same. (Patching node before npm install also segfaults the build, because
/snap/core24/current isn't mounted in the build instance.)prime: excludes trim bulk: koffi non-linux_x64 prebuilds, tree-sitter foreign
prebuilds, the Go stdlib doc/ test/ misc/ trees, etc.A core24 Python app must NOT depend on the host shipping python3.12 — the host may be
newer (Ubuntu 26.04 ships 3.14, with no python3.12 at all). The python plugin also
fails on core24 ("No suitable Python interpreter found", because python3.12 is filtered
as a base-manifest package). So use plugin: nil and build a fully self-contained venv
in override-build:
--copies (real interpreter, not a host symlink),
idempotently so incremental rebuilds don't recreate over a stale venv:
rm -rf "$CRAFT_PART_INSTALL"; mkdir -p "$CRAFT_PART_INSTALL"
python3 -m venv --copies "$CRAFT_PART_INSTALL"
export PATH="$CRAFT_PART_INSTALL/bin:$PATH"
pip install --no-cache-dir <your-packages>lib/ — so just copying files isn't enough on its own, step 3 wires it up):
PYSTDLIB="$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')"
cp -a "${PYSTDLIB}/." "$CRAFT_PART_INSTALL/lib/python3.12/"
rm -rf "$CRAFT_PART_INSTALL/lib/python3.12/test" # large, not needed at runtimehome at the runtime /snap/<name>/current so it finds the
bundled stdlib (use $CRAFT_PROJECT_NAME, never a hardcoded name — a rename
otherwise leaves stale /snap/<old>/... paths):
sed -i \
-e "s|^home = .*|home = /snap/${CRAFT_PROJECT_NAME}/current/bin|" \
-e "s|^executable = .*|executable = /snap/${CRAFT_PROJECT_NAME}/current/bin/python3.12|" \
"$CRAFT_PART_INSTALL/pyvenv.cfg"sed "1s|${CRAFT_PART_INSTALL}|/snap/${CRAFT_PROJECT_NAME}/current|").PYTHONPATH in the launcher. Because step 3 makes the
venv prefix equal its base prefix, Python stops treating it as a venv and Ubuntu's
Debian-patched site.py looks for dist-packages, not the venv's site-packages.
So export it explicitly:
export PYTHONPATH="$SNAP/lib/python3.12/site-packages${PYTHONPATH:+:$PYTHONPATH}"unsquashfs) and running its
bin/python3 with SNAP=<dir> PYTHONPATH=<dir>/lib/python3.12/site-packages — it
should import both an stdlib module (encodings) and an installed package.The daemon is a second app registered as /snap/bin/<name>.daemon; a user unit
ExecStarts it. The CLI launcher installs/refreshes the unit. Use the robust pattern
(re-copy when the bundled unit changes, not just on first run) so snap refreshes land:
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"; SERVICE_NAME="<name>"
UNIT_PATH="$SYSTEMD_USER_DIR/${SERVICE_NAME}.service"; mkdir -p "$SYSTEMD_USER_DIR"
if [ ! -f "$UNIT_PATH" ] || ! cmp -s "$SNAP/share/systemd/user/${SERVICE_NAME}.service" "$UNIT_PATH"; then
cp "$SNAP/share/systemd/user/${SERVICE_NAME}.service" "$UNIT_PATH"
systemctl --user daemon-reload 2>/dev/null || true
fi
if ! systemctl --user is-enabled "$SERVICE_NAME" >/dev/null 2>&1; then
systemctl --user enable --now "$SERVICE_NAME" 2>/dev/null || true
fiUnit: ExecStart=/snap/bin/<name>.daemon …, Restart=always|on-failure,
Environment=HOME=%h, WantedBy=default.target. openclaw uses unit name
<name>-gateway (with legacy-migration from <name>); the lighter snaps still use
bare <name>. Standardizing all on <name>-gateway needs the same migration dance.
snap/**: set up snapd, install snapcraft 8.x/stable,
snapcraft pack, snap install --dangerous --classic, then smoke-test each app
(<name> --version|--help, <name>.daemon --help, <name>.lemonade </dev/null).
Two snapd-setup styles are in use — hand-rolled apt+systemctl (openclaw) vs.
snapcore/prepare-env-gh-action (nullclaw); pick one and unify.snapcraft login --credentials-standard
from secrets.SNAPCRAFT_STORE_CREDENTIALS, snapcraft upload --release=candidate <name>_*.snap.Snap builds need lxd and take many minutes. Before building you can still:
node --check / sh -n every script; python3 -c 'import yaml,...' the YAMLs;
run launcher/picker scripts directly with a faked SNAP/HOME and a real
lemonade-server. Reserve snapcraft pack for final validation.
picoclaw is a CMDOP gRPC client library (strict confinement, no LLM provider, no
CLI entry point yet) — it is NOT an LLM agent, so the lemonade tool does not apply to it.
Don't force a .lemonade app there.
83ae4c2
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.