
Picoclaw Security Guardian
- 54 installs
- 1.1k repo stars
- Updated August 4, 2026
- prompt-security/clawsec
picoclaw-security-guardian is a Claude Code skill for security.
About
picoclaw-security-guardian is a Claude Code skill for security. It helps solo builders move faster with AI-assisted development.
- picoclaw-security-guardian
- Security
- AI-coding skill
Picoclaw Security Guardian by the numbers
- 54 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,271 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/prompt-security/clawsec --skill picoclaw-security-guardianAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 54 |
|---|---|
| repo stars | ★ 1.1k |
| Last updated | August 4, 2026 |
| Repository | prompt-security/clawsec ↗ |
How do I helps with security tasks.?
Helps with security tasks.
Who is it for?
Best when you're working on security and need structured help with picoclaw security guardian.
Skip if: Teams with no security needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with security tasks., or when picoclaw-security-guardian is a claude code skill for security.
What you get
Structured output aligned to picoclaw-security-guardian: picoclaw-security-guardian, Security.
Files
Picoclaw Security Guardian
Detailed architecture/operator docs: wiki/modules/picoclaw-security-guardian.md.
Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
npx skills add prompt-security/clawsec --skill picoclaw-security-guardian -a openclaw -yRelease Artifact Verification
For standalone installs, verify the signed release manifest before trusting SKILL.md, skill.json, or the archive. The skill.json file is the package metadata/SBOM source, and the release pipeline signs checksums.json with the ClawSec release key.
set -euo pipefail
SKILL_NAME="picoclaw-security-guardian"
VERSION="0.0.4"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
RELEASE_PUBKEY_SHA256="711424e4535f84093fefb024cd1ca4ec87439e53907b305b79a631d5befba9c8"
curl -fsSL "$BASE/checksums.json" -o "$TMP_DIR/checksums.json"
curl -fsSL "$BASE/checksums.sig" -o "$TMP_DIR/checksums.sig"
curl -fsSL "$BASE/signing-public.pem" -o "$TMP_DIR/signing-public.pem"
curl -fsSL "$BASE/$ZIP_NAME" -o "$TMP_DIR/$ZIP_NAME"
curl -fsSL "$BASE/SKILL.md" -o "$TMP_DIR/SKILL.md"
curl -fsSL "$BASE/skill.json" -o "$TMP_DIR/skill.json"
ACTUAL_PUBKEY_SHA256="$(openssl pkey -pubin -in "$TMP_DIR/signing-public.pem" -outform DER | shasum -a 256 | awk '{print $1}')"
if [ "$ACTUAL_PUBKEY_SHA256" != "$RELEASE_PUBKEY_SHA256" ]; then
echo "ERROR: signing-public.pem fingerprint mismatch" >&2
exit 1
fi
openssl base64 -d -A -in "$TMP_DIR/checksums.sig" -out "$TMP_DIR/checksums.sig.bin"
openssl pkeyutl -verify -rawin -pubin \
-inkey "$TMP_DIR/signing-public.pem" \
-sigfile "$TMP_DIR/checksums.sig.bin" \
-in "$TMP_DIR/checksums.json" >/dev/null
hash_file() {
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
else
sha256sum "$1" | awk '{print $1}'
fi
}
verify_manifest_file() {
asset="$1"
path="$2"
expected="$(jq -r --arg asset "$asset" '.files[$asset].sha256 // empty' "$TMP_DIR/checksums.json")"
if [ -z "$expected" ]; then
echo "ERROR: checksums.json missing $asset" >&2
exit 1
fi
actual="$(hash_file "$path")"
if [ "$actual" != "$expected" ]; then
echo "ERROR: checksum mismatch for $asset" >&2
exit 1
fi
}
expected_archive="$(jq -r '.archive.sha256 // empty' "$TMP_DIR/checksums.json")"
if [ -z "$expected_archive" ]; then
echo "ERROR: checksums.json missing archive.sha256" >&2
exit 1
fi
actual_archive="$(hash_file "$TMP_DIR/$ZIP_NAME")"
if [ "$actual_archive" != "$expected_archive" ]; then
echo "ERROR: archive checksum mismatch" >&2
exit 1
fi
verify_manifest_file "SKILL.md" "$TMP_DIR/SKILL.md"
verify_manifest_file "skill.json" "$TMP_DIR/skill.json"
echo "Signed release manifest, archive, SKILL.md, and skill.json verified."Only install or extract the archive after this verification succeeds.
Goal
Provide Picoclaw with the same support-matrix security capabilities ClawSec tracks for mature platform modules:
| Skill name | supported platform | security feed | config drift | agent posture-review lane | chain of supply verification |
|---|---|---|---|---|---|
| picoclaw-security-guardian | Picoclaw | Yes | Yes | Separate package | Yes |
Threat model
Picoclaw is a lightweight AI gateway that can expose chat channels, a Web UI, tool execution, MCP servers, credentials, schedulers, and embedded/router deployments. This skill focuses on the trust boundaries where those features become security-sensitive.
Default safety posture
- Read-only by default.
- No scheduler creation in v0.0.1.
- No outbound network by default.
- Writes only explicit report/profile outputs under
$PICOCLAW_HOME/security/clawsec/unless the operator supplies test-local temporary paths. - Advisory checks fail closed when verification state is not verified unless the operator passes
--allow-unsignedfor a documented emergency/offline window.
Security advisory awareness
Use scripts/check_advisories.mjs with a local feed/cache and verification state:
node scripts/check_advisories.mjs --feed ~/.picoclaw/security/clawsec/feed.json --state ~/.picoclaw/security/clawsec/feed-verification-state.jsonThe script filters advisories for picoclaw, ai-gateway, empty/all-platform advisories, or affected package entries containing picoclaw. The expected feed input is the consolidated signed ClawSec advisory feed, so it can contain NVD CVEs, approved community advisories, and provisional GHSA-without-CVE records.
Drift protection
Generate a deterministic profile:
node scripts/generate_profile.mjs --output ~/.picoclaw/security/clawsec/current-profile.jsonCompare against an approved baseline:
node scripts/check_drift.mjs --baseline ~/.picoclaw/security/clawsec/baseline-profile.json --current ~/.picoclaw/security/clawsec/current-profile.json --fail-on criticalCritical drift includes public Web UI enablement, Web UI auth disablement, workspace restriction disablement, unsigned/insecure verification mode, verified-feed regression, and watched-file/release-artifact fingerprint changes.
Chain-of-supply verification
Verify a Picoclaw release artifact against a checksum manifest plus detached signature. Signed manifest verification is required for a passing supply-chain verdict:
node scripts/verify_supply_chain.mjs \
--artifact ./picoclaw \
--checksums ./checksums.json \
--signature ./checksums.json.sig \
--public-key ./feed-signing-public.pemChecksum-only mode is integrity-only, not provenance. Use --allow-unsigned-checksums only for short, documented offline triage windows; it should not satisfy production install verification.
Operator review notes
- Treat public UI binding (
0.0.0.0,-public) as a critical review item until auth and network allowlists are proven. - Treat MCP servers as separate trust boundaries; review each server's filesystem, network, and credential access.
- Treat third-party OpenWrt/LuCI wrappers as separate supply-chain artifacts. Verify provenance before installing them on routers.
- Never leave unsigned advisory mode enabled in recurring or production checks.
Validation
python utils/validate_skill.py skills/picoclaw-security-guardian
node skills/picoclaw-security-guardian/test/profile.test.mjs
node skills/picoclaw-security-guardian/test/drift.test.mjs
node skills/picoclaw-security-guardian/test/supply_chain.test.mjs
bash -n skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.shPre-release install regression
Before publishing v0.0.1 release artifacts, run the isolated install lane from the repo root:
skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.shThe regression installs the skill through Picoclaw's own find_skills / install_skill path from a local ClawHub-compatible registry into an isolated Docker-hosted Picoclaw workspace with isolated HOME, PICOCLAW_HOME, and PICOCLAW_WORKSPACE. It verifies signed release-artifact preflight inputs, confirms Picoclaw's skill loader can list/load the installed skill, then runs the installed copy's profile, drift, advisory fail-closed, advisory filtering, and supply-chain verification paths against Picoclaw-style config.json and launcher-config.json files.
Changelog
[0.0.4] - 2026-06-10
Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
[0.0.3] - 2026-05-24
Changed
- Documented that Picoclaw advisory checks consume the consolidated signed advisory feed, including NVD CVEs, approved community advisories, and provisional GHSA-without-CVE records.
[0.0.2] - 2026-05-13
Security
- Added explicit signed release artifact verification instructions for standalone installs, including
checksums.json,checksums.sig,signing-public.pem, archive hash verification, andSKILL.md/skill.jsonchecksum checks.
Changed
- Re-release skill payload metadata after excluding test-only files from release SBOMs and archives.
[0.0.1] - 2026-04-26
Added
- Initial Picoclaw-specific ClawSec skill package for advisory awareness, deterministic profile generation, drift detection, and supply-chain verification.
- Picoclaw-native Docker pre-release install regression harness using
find_skills/install_skilland skill-loader validation.
Changed
- Split optional posture-review checks into separate
picoclaw-self-pen-testingpackage so this package remains the core public guardian lane. - Updated metadata/docs/regression expectations to keep this package focused on advisory, drift, and supply-chain checks.
import fs from "node:fs";
export function loadAdvisoryFeed(feedPath) { return JSON.parse(fs.readFileSync(feedPath, "utf8")); }
export function loadFeedState(statePath) { if (!statePath || !fs.existsSync(statePath)) return { status: "unknown" }; return JSON.parse(fs.readFileSync(statePath, "utf8")); }
export function isPicoclawAdvisory(advisory) {
const platforms = Array.isArray(advisory?.platforms) ? advisory.platforms.map(x=>String(x).toLowerCase()) : [];
const affected = Array.isArray(advisory?.affected) ? advisory.affected.map(x=>String(x).toLowerCase()) : [];
const blob = `${advisory?.title || ""} ${advisory?.description || ""} ${advisory?.type || ""}`.toLowerCase();
return platforms.length === 0 || platforms.includes("picoclaw") || platforms.includes("ai-gateway") || affected.some(x=>x.includes("picoclaw")) || blob.includes("picoclaw");
}
export function checkPicoclawAdvisories({ feedPath, statePath, allowUnsigned = false }) {
const state = loadFeedState(statePath);
if (!allowUnsigned && state.status !== "verified") throw new Error(`advisory feed state is not verified: ${state.status || "missing"}`);
const feed = loadAdvisoryFeed(feedPath);
const advisories = (feed.advisories || []).filter(isPicoclawAdvisory);
return { status: "ok", feed_version: feed.version || null, verified_state: state.status || "unknown", count: advisories.length, advisories };
}
const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
function bump(summary, sev) { summary[sev] = (summary[sev] || 0) + 1; }
function bool(value) { return !!value; }
function add(findings, summary, severity, code, path, message, details = undefined) {
const finding = { severity, code, path, message };
if (details) finding.details = details;
findings.push(finding); bump(summary, severity);
}
function byPath(entries) { const m = new Map(); for (const e of Array.isArray(entries) ? entries : []) if (e?.path) m.set(e.path, e); return m; }
function compareBool({ before, after, path, codeOnEnable, codeOnDisable, enableSeverity, findings, summary }) {
if (bool(before) === bool(after)) return;
if (!before && after) add(findings, summary, enableSeverity, codeOnEnable, path, `${path} changed false -> true`);
else add(findings, summary, "info", codeOnDisable, path, `${path} changed true -> false`);
}
function compareHashSet(beforeEntries, afterEntries, changedCode, removedCode, findings, summary) {
const b = byPath(beforeEntries); const a = byPath(afterEntries);
for (const [p, before] of b.entries()) {
const after = a.get(p);
if (!after) { add(findings, summary, "high", removedCode, p, `${p} missing from current profile`); continue; }
if ((before.sha256 || null) !== (after.sha256 || null)) add(findings, summary, "critical", changedCode, p, `${p} fingerprint changed`);
}
for (const [p] of a.entries()) if (!b.has(p)) add(findings, summary, "low", "NEW_INTEGRITY_SCOPE", p, `${p} added to integrity tracking scope`);
}
export function diffPicoclawProfiles(baseline, current) {
const findings=[]; const summary={critical:0, high:0, medium:0, low:0, info:0};
const b=baseline||{}; const c=current||{};
if (b.platform !== c.platform) add(findings, summary, "critical", "PLATFORM_MISMATCH", "platform", `platform changed ${b.platform} -> ${c.platform}`);
if (b.schema_version !== c.schema_version) add(findings, summary, "high", "SCHEMA_VERSION_CHANGED", "schema_version", `schema_version changed ${b.schema_version} -> ${c.schema_version}`);
const br=b.posture?.runtime||{}; const cr=c.posture?.runtime||{};
compareBool({before: br.ui?.public_web_ui, after: cr.ui?.public_web_ui, path:"posture.runtime.ui.public_web_ui", codeOnEnable:"PUBLIC_WEB_UI_ENABLED", codeOnDisable:"PUBLIC_WEB_UI_DISABLED", enableSeverity:"critical", findings, summary});
compareBool({before: br.ui?.auth_disabled, after: cr.ui?.auth_disabled, path:"posture.runtime.ui.auth_disabled", codeOnEnable:"WEB_UI_AUTH_DISABLED", codeOnDisable:"WEB_UI_AUTH_REENABLED", enableSeverity:"critical", findings, summary});
compareBool({before: br.tools?.unrestricted_workspace, after: cr.tools?.unrestricted_workspace, path:"posture.runtime.tools.unrestricted_workspace", codeOnEnable:"WORKSPACE_RESTRICTION_DISABLED", codeOnDisable:"WORKSPACE_RESTRICTION_RESTORED", enableSeverity:"critical", findings, summary});
compareBool({before: br.risky_toggles?.allow_unsigned_mode, after: cr.risky_toggles?.allow_unsigned_mode, path:"posture.runtime.risky_toggles.allow_unsigned_mode", codeOnEnable:"UNSIGNED_MODE_ENABLED", codeOnDisable:"UNSIGNED_MODE_DISABLED", enableSeverity:"critical", findings, summary});
compareBool({before: br.mcp?.enabled, after: cr.mcp?.enabled, path:"posture.runtime.mcp.enabled", codeOnEnable:"MCP_ENABLED", codeOnDisable:"MCP_DISABLED", enableSeverity:"high", findings, summary});
compareBool({before: br.scheduler?.enabled, after: cr.scheduler?.enabled, path:"posture.runtime.scheduler.enabled", codeOnEnable:"SCHEDULER_ENABLED", codeOnDisable:"SCHEDULER_DISABLED", enableSeverity:"medium", findings, summary});
if ((br.secrets?.config_secret_markers||0) < (cr.secrets?.config_secret_markers||0)) add(findings, summary, "high", "SECRET_MARKERS_INCREASED", "posture.runtime.secrets.config_secret_markers", "config secret markers increased", { before: br.secrets?.config_secret_markers||0, after: cr.secrets?.config_secret_markers||0 });
if (b.posture?.feed_verification?.status === "verified" && c.posture?.feed_verification?.status !== "verified") add(findings, summary, "critical", "FEED_VERIFICATION_REGRESSION", "posture.feed_verification.status", `Feed verification regressed verified -> ${c.posture?.feed_verification?.status || "unknown"}`);
compareHashSet(b.posture?.integrity?.watched_files, c.posture?.integrity?.watched_files, "WATCHED_FILE_DRIFT", "WATCHED_FILE_REMOVED", findings, summary);
compareHashSet(b.posture?.integrity?.release_artifacts, c.posture?.integrity?.release_artifacts, "RELEASE_ARTIFACT_DRIFT", "RELEASE_ARTIFACT_REMOVED", findings, summary);
findings.sort((x,y)=>SEVERITY_ORDER.indexOf(x.severity)-SEVERITY_ORDER.indexOf(y.severity)||String(x.code).localeCompare(String(y.code))||String(x.path).localeCompare(String(y.path)));
return { summary, findings };
}
export function highestSeverity(findings=[]) { return SEVERITY_ORDER.find(s => findings.some(f => f?.severity===s)) || null; }
export function severityAtOrAbove(severity, threshold) { if (!threshold || threshold === "none") return false; const a=SEVERITY_ORDER.indexOf(severity), b=SEVERITY_ORDER.indexOf(threshold); return a >= 0 && b >= 0 && a <= b; }
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
export const SCHEMA_VERSION = "picoclaw-profile/v1";
export const PROFILE_VERSION = "0.0.1";
export function stableStringify(value, space = 2) {
return JSON.stringify(sortDeep(value), null, space);
}
function sortDeep(value) {
if (Array.isArray(value)) return value.map(sortDeep);
if (!value || typeof value !== "object") return value;
const out = {};
for (const key of Object.keys(value).sort()) out[key] = sortDeep(value[key]);
return out;
}
export function sha256Hex(content) {
return crypto.createHash("sha256").update(content).digest("hex");
}
export function sha256FileHex(filePath) {
return sha256Hex(fs.readFileSync(filePath));
}
export function defaultPicoclawHome() {
return path.resolve(process.env.PICOCLAW_HOME || path.join(os.homedir(), ".picoclaw"));
}
export function defaultOutputPath(picoclawHome = defaultPicoclawHome()) {
return path.join(picoclawHome, "security", "clawsec", "current-profile.json");
}
export function expandUserPath(raw, base = defaultPicoclawHome()) {
if (!raw) return "";
const value = String(raw).trim();
if (!value) return "";
if (value === "~") return os.homedir();
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
if (value.startsWith("$PICOCLAW_HOME/")) return path.join(base, value.slice("$PICOCLAW_HOME/".length));
return path.resolve(value);
}
export function isPathInside(childPath, parentPath) {
const child = path.resolve(childPath);
const parent = path.resolve(parentPath);
const rel = path.relative(parent, child);
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
}
function nearestExistingAncestor(candidatePath) {
let candidate = path.resolve(candidatePath);
while (!fs.existsSync(candidate)) {
const parent = path.dirname(candidate);
if (parent === candidate) return candidate;
candidate = parent;
}
return candidate;
}
function realpathWithMissingTail(candidatePath) {
const resolved = path.resolve(candidatePath);
const ancestor = nearestExistingAncestor(resolved);
const realAncestor = fs.realpathSync.native ? fs.realpathSync.native(ancestor) : fs.realpathSync(ancestor);
const rel = path.relative(ancestor, resolved);
return rel ? path.join(realAncestor, rel) : realAncestor;
}
export function confineOutputToPicoclawHome(candidatePath, picoclawHome = defaultPicoclawHome()) {
const root = path.resolve(picoclawHome);
const resolved = path.resolve(candidatePath);
if (!isPathInside(resolved, root)) throw new Error(`output path must stay under ${root}`);
const rootReal = realpathWithMissingTail(root);
const resolvedReal = realpathWithMissingTail(resolved);
if (!isPathInside(resolvedReal, rootReal)) throw new Error(`output path must stay under ${rootReal}`);
if (fs.existsSync(resolved) && fs.lstatSync(resolved).isSymbolicLink()) {
throw new Error(`output path must not be a symlink: ${resolved}`);
}
return resolved;
}
export function parseJsonFile(filePath) {
if (!filePath || !fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
export function detectConfigPaths(picoclawHome = defaultPicoclawHome(), extraConfig = null) {
const candidates = [
process.env.PICOCLAW_CONFIG,
extraConfig,
path.join(picoclawHome, "config.yaml"),
path.join(picoclawHome, "config.yml"),
path.join(picoclawHome, "config.json"),
path.join(picoclawHome, "launcher-config.json"),
path.join(picoclawHome, ".security.yml"),
path.join(picoclawHome, "security.yml"),
].filter(Boolean).map((p) => expandUserPath(p, picoclawHome));
return [...new Set(candidates)];
}
function safeReadText(filePath, maxBytes = 1024 * 1024) {
try {
const st = fs.statSync(filePath);
if (!st.isFile() || st.size > maxBytes) return "";
return fs.readFileSync(filePath, "utf8");
} catch {
return "";
}
}
function fingerprintPath(filePath) {
const exists = fs.existsSync(filePath);
if (!exists) return { path: filePath, exists: false };
const st = fs.statSync(filePath);
return {
path: filePath,
exists: true,
type: st.isDirectory() ? "directory" : st.isFile() ? "file" : "other",
size: st.isFile() ? st.size : null,
mode: (st.mode & 0o777).toString(8).padStart(3, "0"),
sha256: st.isFile() ? sha256FileHex(filePath) : null,
};
}
function truthyFromText(text, patterns) {
const low = text.toLowerCase();
return patterns.some((p) => low.includes(p));
}
function truthyRegex(text, patterns) {
return patterns.some((p) => p.test(text));
}
function jsonBoolPattern(key, expected) {
return new RegExp(`"${key}"\\s*:\\s*${expected ? "true" : "false"}`, "i");
}
function jsonEmptyStringPattern(key) {
return new RegExp(`"${key}"\\s*:\\s*"\\s*"`, "i");
}
function jsonStringPattern(key, value) {
return new RegExp(`"${key}"\\s*:\\s*"${value.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}"`, "i");
}
function analyzeConfigText(text) {
return {
public_web_ui: truthyFromText(text, [
"public: true",
"bind: 0.0.0.0",
"host: 0.0.0.0",
"-public",
'"public": true',
'"bind": "0.0.0.0"',
'"host": "0.0.0.0"',
'"listen": "0.0.0.0"',
]) || truthyRegex(text, [
jsonBoolPattern("public", true),
jsonStringPattern("bind", "0.0.0.0"),
jsonStringPattern("host", "0.0.0.0"),
jsonStringPattern("listen", "0.0.0.0"),
]),
auth_disabled: truthyFromText(text, [
"auth: false",
"disable_auth: true",
"no_auth: true",
"password: ''",
'password: ""',
'"auth": false',
'"disable_auth": true',
'"no_auth": true',
'"require_auth": false',
'"dashboard_auth": false',
'"password": ""',
'"dashboard_password_hash": ""',
'"launcher_token": ""',
]) || truthyRegex(text, [
jsonBoolPattern("auth", false),
jsonBoolPattern("disable_auth", true),
jsonBoolPattern("no_auth", true),
jsonBoolPattern("require_auth", false),
jsonBoolPattern("dashboard_auth", false),
jsonEmptyStringPattern("password"),
jsonEmptyStringPattern("dashboard_password_hash"),
jsonEmptyStringPattern("launcher_token"),
]),
allow_unsigned: truthyFromText(text, [
"allow_unsigned",
"skip_signature",
"disable_signature",
"insecure_skip_verify",
]),
unrestricted_workspace: truthyFromText(text, [
"restrict_to_workspace: false",
"workspace_restriction: false",
"sandbox: false",
'"restrict_to_workspace": false',
'"workspace_restriction": false',
'"sandbox": false',
]) || truthyRegex(text, [
jsonBoolPattern("restrict_to_workspace", false),
jsonBoolPattern("workspace_restriction", false),
jsonBoolPattern("sandbox", false),
]),
mcp_enabled: truthyFromText(text, ["mcp:", "mcp_servers", "modelcontextprotocol", '"mcp"', '"mcp_servers"']),
tools_enabled: truthyFromText(text, ["tools:", "code_execution", "shell", "filesystem", '"tools"', '"exec"', '"shell"']),
scheduler_enabled: truthyFromText(text, ["cron", "schedule", "scheduler"]),
secret_markers: (text.match(/(api[_-]?key|token|secret|password)\s*[":=]+\s*['"]?[^\s'"]{8,}/gi) || []).length,
};
}
function mergeConfigSignals(paths) {
const signals = {
public_web_ui: false,
auth_disabled: false,
allow_unsigned: false,
unrestricted_workspace: false,
mcp_enabled: false,
tools_enabled: false,
scheduler_enabled: false,
secret_markers: 0,
};
for (const p of paths) {
const text = safeReadText(p);
const found = analyzeConfigText(text);
for (const [k, v] of Object.entries(found)) {
if (typeof v === "boolean") signals[k] = signals[k] || v;
else signals[k] += v;
}
}
return signals;
}
export function buildPicoclawProfile(options = {}) {
const picoclawHome = path.resolve(options.picoclawHome || defaultPicoclawHome());
const generatedAt = options.generatedAt || new Date().toISOString();
const configPaths = detectConfigPaths(picoclawHome, options.configPath);
const watchedFiles = [...new Set([...(options.watchFiles || []), ...configPaths].filter(Boolean).map((p) => expandUserPath(p, picoclawHome)))];
const releaseArtifacts = [...new Set((options.releaseArtifacts || []).filter(Boolean).map((p) => expandUserPath(p, picoclawHome)))];
const signals = options.signals || mergeConfigSignals(watchedFiles);
const profile = {
schema_version: SCHEMA_VERSION,
platform: "picoclaw",
generated_at: generatedAt,
generator: { name: "picoclaw-security-guardian", version: PROFILE_VERSION },
posture: {
runtime: {
home: picoclawHome,
config_paths: configPaths,
gateways: options.gateways || {},
ui: { public_web_ui: !!signals.public_web_ui, auth_disabled: !!signals.auth_disabled },
tools: { enabled: !!signals.tools_enabled, unrestricted_workspace: !!signals.unrestricted_workspace },
mcp: { enabled: !!signals.mcp_enabled },
scheduler: { enabled: !!signals.scheduler_enabled },
risky_toggles: { allow_unsigned_mode: !!signals.allow_unsigned },
secrets: { config_secret_markers: signals.secret_markers || 0 },
},
integrity: {
watched_files: watchedFiles.map(fingerprintPath),
release_artifacts: releaseArtifacts.map(fingerprintPath),
},
feed_verification: options.feedVerification || { status: "unknown" },
},
};
profile.digests = { canonical_sha256: sha256Hex(stableStringify({ ...profile, digests: undefined }, 0)) };
return profile;
}
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { sha256FileHex } from "./profile.mjs";
function normalizeManifestPath(value) {
return String(value || "").trim().replace(/^\.\//, "");
}
function parseChecksums(raw) {
const text = String(raw || "");
const trimmed = text.trim();
if (!trimmed) throw new Error("checksum manifest is empty");
if (trimmed.startsWith("{")) {
const parsed = JSON.parse(trimmed);
const source = parsed.files && typeof parsed.files === "object" ? parsed.files : parsed;
const out = {};
for (const [manifestPath, entry] of Object.entries(source)) {
const normalized = normalizeManifestPath(manifestPath);
const hash = typeof entry === "string" ? entry : entry?.sha256;
if (typeof hash === "string" && /^[a-fA-F0-9]{64}$/.test(hash.trim())) {
if (out[normalized]) throw new Error(`duplicate checksum entry: ${normalized}`);
out[normalized] = hash.trim().toLowerCase();
}
}
return out;
}
const out = {};
const basenameCounts = new Map();
for (const line of text.split(/\r?\n/)) {
const m = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
if (!m) continue;
const manifestPath = normalizeManifestPath(m[2]);
if (out[manifestPath]) throw new Error(`duplicate checksum entry: ${manifestPath}`);
out[manifestPath] = m[1].toLowerCase();
const base = path.basename(manifestPath);
basenameCounts.set(base, (basenameCounts.get(base) || 0) + 1);
}
for (const [base, count] of basenameCounts.entries()) {
if (count > 1) throw new Error(`ambiguous duplicate checksum basename: ${base}`);
}
return out;
}
function expectedForArtifact(files, artifactPath, manifestName = null) {
const candidates = [manifestName, artifactPath, path.basename(artifactPath)]
.filter(Boolean)
.map(normalizeManifestPath);
for (const candidate of candidates) {
if (files[candidate]) return files[candidate];
}
return null;
}
export function verifyChecksums({ artifactPath, checksumsPath, manifestName = null }) {
const files = parseChecksums(fs.readFileSync(checksumsPath, "utf8"));
const expected = expectedForArtifact(files, artifactPath, manifestName);
if (!expected) {
return { ok: false, status: "missing", artifact: artifactPath, message: "artifact not present in checksum manifest" };
}
const actual = sha256FileHex(artifactPath);
return { ok: actual === expected, status: actual === expected ? "verified" : "mismatch", artifact: artifactPath, expected, actual };
}
export function verifyDetachedSignature({ manifestPath, signaturePath, publicKeyPath }) {
const manifestBytes = fs.readFileSync(manifestPath);
const signatureText = fs.readFileSync(signaturePath, "utf8").trim();
const sig = Buffer.from(signatureText.replace(/\s+/g, ""), "base64");
const key = crypto.createPublicKey(fs.readFileSync(publicKeyPath, "utf8"));
const ok = crypto.verify(null, manifestBytes, key, sig);
return { ok, status: ok ? "verified" : "mismatch", manifest: manifestPath, signature: signaturePath };
}
export function verifySupplyChain(options) {
const checksum = verifyChecksums(options);
if (!options.allowUnsignedChecksums && (!options.signaturePath || !options.publicKeyPath)) {
return {
checksum,
signature: { ok: false, status: "missing" },
ok: false,
message: "detached signature and trusted public key are required for supply-chain verification",
};
}
const result = { checksum, signature: { ok: null, status: "not_checked" }, ok: checksum.ok };
if (options.signaturePath && options.publicKeyPath) {
result.signature = verifyDetachedSignature({
manifestPath: options.checksumsPath,
signaturePath: options.signaturePath,
publicKeyPath: options.publicKeyPath,
});
result.ok = checksum.ok && result.signature.ok;
} else {
result.signature = { ok: null, status: "unsigned_checksum_only" };
result.ok = checksum.ok;
}
return result;
}
picoclaw-security-guardian
Picoclaw security posture skill for ClawSec.
Status: implemented (v0.0.1), Picoclaw-specific.
Detailed architecture/operator docs: wiki/modules/picoclaw-security-guardian.md.
Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
npx skills add prompt-security/clawsec --skill picoclaw-security-guardian -a openclaw -ySupport matrix mapping
| Skill name | supported platform | security feed | config drift | agent posture-review lane | chain of supply verification |
|---|---|---|---|---|---|
| picoclaw-security-guardian | Picoclaw | Yes | Yes | Separate package | Yes |
Capabilities
- Picoclaw-aware advisory filtering from a verified ClawSec feed/cache.
- Deterministic local posture profile generation for configs, gateway exposure, tools, MCP, credentials/security files, and release artifacts.
- Baseline drift comparison with critical/high/medium/low/info findings.
- Supply-chain verification for release artifacts using SHA-256 manifests plus required Ed25519 detached signatures for passing provenance verdicts.
Quickstart
node scripts/generate_profile.mjs --output ~/.picoclaw/security/clawsec/current-profile.json
node scripts/check_drift.mjs --baseline ~/.picoclaw/security/clawsec/baseline-profile.json --current ~/.picoclaw/security/clawsec/current-profile.json
node scripts/verify_supply_chain.mjs --artifact ./picoclaw --checksums ./checksums.json --signature ./checksums.json.sig --public-key ./feed-signing-public.pem
node scripts/check_advisories.mjs --feed ~/.picoclaw/security/clawsec/feed.json --state ~/.picoclaw/security/clawsec/feed-verification-state.jsonAll scripts are read-only except profile/report outputs explicitly requested by --output.
Tests
node test/profile.test.mjs
node test/drift.test.mjs
node test/supply_chain.test.mjs
bash -n test/picoclaw_security_guardian_sandbox_regression.shPre-release install regression
Run this before cutting v0.0.1 release artifacts:
test/picoclaw_security_guardian_sandbox_regression.shIt uses Docker to publish the skill through a local ClawHub-compatible registry, installs it with Picoclaw's own find_skills / install_skill flow into an isolated Picoclaw workspace, confirms Picoclaw's skill loader can list/load it, then verifies the installed copy's profile, drift, advisory, and supply-chain paths.
#!/usr/bin/env node
import { checkPicoclawAdvisories } from "../lib/advisories.mjs"; import { stableStringify } from "../lib/profile.mjs";
function parse(argv){const a={allowUnsigned:false}; for(let i=0;i<argv.length;i++){const t=argv[i]; if(t==="--feed") a.feedPath=argv[++i]; else if(t==="--state") a.statePath=argv[++i]; else if(t==="--allow-unsigned") a.allowUnsigned=true; else throw new Error(`Unknown argument: ${t}`);} if(!a.feedPath) throw new Error("--feed is required"); return a;}
const result=checkPicoclawAdvisories(parse(process.argv.slice(2))); console.log(stableStringify(result));
#!/usr/bin/env node
import fs from "node:fs"; import { diffPicoclawProfiles, highestSeverity, severityAtOrAbove } from "../lib/drift.mjs"; import { stableStringify } from "../lib/profile.mjs";
function parse(argv){const a={failOn:"critical"}; for(let i=0;i<argv.length;i++){const t=argv[i]; if(t==="--baseline") a.baseline=argv[++i]; else if(t==="--current") a.current=argv[++i]; else if(t==="--fail-on") a.failOn=argv[++i]; else throw new Error(`Unknown argument: ${t}`);} if(!a.baseline||!a.current) throw new Error("--baseline and --current are required"); return a;}
const a=parse(process.argv.slice(2)); const result=diffPicoclawProfiles(JSON.parse(fs.readFileSync(a.baseline,"utf8")), JSON.parse(fs.readFileSync(a.current,"utf8"))); console.log(stableStringify(result)); const hi=highestSeverity(result.findings); if(severityAtOrAbove(hi,a.failOn)) process.exit(2);
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { buildPicoclawProfile, confineOutputToPicoclawHome, defaultOutputPath, defaultPicoclawHome, stableStringify } from "../lib/profile.mjs";
function parse(argv) {
const args = { watch: [], artifact: [], output: null, home: defaultPicoclawHome(), generatedAt: null, config: null };
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--output") args.output = argv[++i];
else if (token === "--home") args.home = argv[++i];
else if (token === "--watch") args.watch.push(argv[++i]);
else if (token === "--artifact") args.artifact.push(argv[++i]);
else if (token === "--generated-at") args.generatedAt = argv[++i];
else if (token === "--config") args.config = argv[++i];
else if (token === "--help") {
console.log("Usage: node scripts/generate_profile.mjs [--output path] [--home path] [--config path] [--watch path] [--artifact path]");
process.exit(0);
} else {
throw new Error(`Unknown argument: ${token}`);
}
}
if (!args.output) args.output = defaultOutputPath(args.home);
return args;
}
function writeNoFollow(outPath, body) {
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0);
const fd = fs.openSync(outPath, flags, 0o600);
try {
fs.writeFileSync(fd, body, "utf8");
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
}
const args = parse(process.argv.slice(2));
const profile = buildPicoclawProfile({
picoclawHome: args.home,
generatedAt: args.generatedAt,
configPath: args.config,
watchFiles: args.watch,
releaseArtifacts: args.artifact,
});
const out = confineOutputToPicoclawHome(args.output, args.home);
fs.mkdirSync(path.dirname(out), { recursive: true, mode: 0o700 });
const checkedOut = confineOutputToPicoclawHome(out, args.home);
writeNoFollow(checkedOut, `${stableStringify(profile)}\n`);
console.log(stableStringify({ message: "picoclaw profile generated", output: checkedOut, canonical_sha256: profile.digests.canonical_sha256 }, 0));
#!/usr/bin/env node
import { verifySupplyChain } from "../lib/supply_chain.mjs";
import { stableStringify } from "../lib/profile.mjs";
function parse(argv) {
const args = { allowUnsignedChecksums: false, manifestName: null };
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--artifact") args.artifactPath = argv[++i];
else if (token === "--checksums") args.checksumsPath = argv[++i];
else if (token === "--signature") args.signaturePath = argv[++i];
else if (token === "--public-key") args.publicKeyPath = argv[++i];
else if (token === "--manifest-name") args.manifestName = argv[++i];
else if (token === "--allow-unsigned-checksums") args.allowUnsignedChecksums = true;
else throw new Error(`Unknown argument: ${token}`);
}
if (!args.artifactPath || !args.checksumsPath) throw new Error("--artifact and --checksums are required");
return args;
}
const result = verifySupplyChain(parse(process.argv.slice(2)));
console.log(stableStringify(result));
if (!result.ok) process.exit(2);
{
"name": "picoclaw-security-guardian",
"version": "0.0.4",
"description": "Picoclaw security posture skill with advisory awareness, configuration drift detection, and supply-chain verification guidance.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
"homepage": "https://clawsec.prompt.security/",
"platform": "picoclaw",
"keywords": [
"security",
"picoclaw",
"ai-gateway",
"advisory",
"drift-detection",
"supply-chain"
],
"sbom": {
"files": [
{
"path": "SKILL.md",
"required": true,
"description": "Skill documentation and Picoclaw operator playbook"
},
{
"path": "README.md",
"required": true,
"description": "Human-oriented overview and quickstart"
},
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history and release notes"
},
{
"path": "lib/profile.mjs",
"required": true,
"description": "Picoclaw posture profile and path-confinement helpers"
},
{
"path": "lib/drift.mjs",
"required": true,
"description": "Baseline comparison and severity mapping helpers"
},
{
"path": "lib/supply_chain.mjs",
"required": true,
"description": "Release artifact checksum/signature verification helpers"
},
{
"path": "lib/advisories.mjs",
"required": true,
"description": "Picoclaw advisory feed filtering helpers"
},
{
"path": "scripts/generate_profile.mjs",
"required": true,
"description": "Generate deterministic Picoclaw security posture profile"
},
{
"path": "scripts/check_drift.mjs",
"required": true,
"description": "Compare Picoclaw profile against an approved baseline"
},
{
"path": "scripts/verify_supply_chain.mjs",
"required": true,
"description": "Verify release artifact checksums and required detached signatures for provenance"
},
{
"path": "scripts/check_advisories.mjs",
"required": true,
"description": "Check Picoclaw-relevant advisories from a signed/verified feed state"
}
]
},
"picoclaw": {
"emoji": "🦐",
"category": "security",
"requires": {
"bins": [
"node"
]
},
"runtime": {
"required_env": [],
"optional_env": [
"PICOCLAW_HOME",
"PICOCLAW_CONFIG",
"PICOCLAW_PROFILE_OUTPUT_DIR",
"PICOCLAW_BASELINE",
"PICOCLAW_ADVISORY_FEED_STATE_PATH",
"PICOCLAW_ADVISORY_CACHED_FEED",
"PICOCLAW_ALLOW_UNSIGNED_FEED"
]
},
"capabilities": {
"security_feed": true,
"config_drift": true,
"agent_self_pen_testing": false,
"supply_chain_install_verification": true
},
"execution": {
"always": false,
"persistence": "Read-only/on-demand in v0.0.1; no scheduler is installed.",
"network_egress": "None by default. Advisory checks consume local verified feed state/cache unless the operator supplies a feed file."
},
"operator_review": [
"Picoclaw-specific skill: use for Picoclaw gateways and lightweight AI gateway deployments, not OpenClaw hook execution.",
"Treat public Web UI binding and broad chat-channel enablement as review findings until explicitly justified.",
"Keep unsigned advisory mode temporary and documented; default workflows expect verified feed state.",
"Supply-chain verification requires manifests/signatures from a trusted release source; third-party LuCI wrappers need separate provenance review."
],
"triggers": [
"picoclaw security profile",
"picoclaw drift detection",
"picoclaw advisory check",
"picoclaw supply chain verification"
],
"test_requires": {
"bins": [
"bash",
"docker",
"python3",
"node",
"openssl",
"zip"
]
}
}
}
import assert from "node:assert/strict"; import { diffPicoclawProfiles, highestSeverity } from "../lib/drift.mjs";
const base={platform:"picoclaw",schema_version:"picoclaw-profile/v1",posture:{runtime:{ui:{public_web_ui:false,auth_disabled:false},tools:{unrestricted_workspace:false},mcp:{enabled:false},scheduler:{enabled:false},risky_toggles:{allow_unsigned_mode:false},secrets:{config_secret_markers:0}},feed_verification:{status:"verified"},integrity:{watched_files:[{path:"a",sha256:"1"}],release_artifacts:[]}}};
const cur=JSON.parse(JSON.stringify(base)); cur.posture.runtime.ui.public_web_ui=true; cur.posture.feed_verification.status="unknown"; cur.posture.integrity.watched_files[0].sha256="2";
const d=diffPicoclawProfiles(base,cur); assert.equal(highestSeverity(d.findings),"critical"); assert.ok(d.findings.some(f=>f.code==="PUBLIC_WEB_UI_ENABLED")); assert.ok(d.findings.some(f=>f.code==="FEED_VERIFICATION_REGRESSION")); assert.ok(d.findings.some(f=>f.code==="WATCHED_FILE_DRIFT")); console.log("drift.test.mjs PASS");
#!/usr/bin/env bash
set -euo pipefail
# Picoclaw-oriented sandbox regression for picoclaw-security-guardian.
#
# This is deliberately NOT a Hermes install test. It boots a disposable Docker
# sandbox, mounts a Picoclaw source tree, publishes this skill through a local
# ClawHub-compatible registry, installs it with Picoclaw's own install_skill tool,
# verifies Picoclaw's skill loader can see/load it, then runs the installed copy's
# Picoclaw security workflows against an isolated PICOCLAW_HOME.
#
# Usage from the ClawSec repo root:
# skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.sh
#
# Optional env overrides:
# IMAGE=golang:1.25-bookworm
# PICOCLAW_SRC=/home/davida/picoclaw_research/picoclaw
# SKILL_SRC=/home/davida/clawsec/skills/picoclaw-security-guardian
# CLAWHUB_PORT=8767
IMAGE="${IMAGE:-golang:1.25-bookworm}"
PICOCLAW_SRC="${PICOCLAW_SRC:-$HOME/picoclaw_research/picoclaw}"
SKILL_SRC="${SKILL_SRC:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
CLAWHUB_PORT="${CLAWHUB_PORT:-8767}"
SKILL_VERSION="${SKILL_VERSION:-$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["version"])' "$SKILL_SRC/skill.json")}"
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: docker is required." >&2
exit 1
fi
if [[ ! -d "$PICOCLAW_SRC" ]]; then
echo "ERROR: PICOCLAW_SRC not found: $PICOCLAW_SRC" >&2
exit 1
fi
if [[ ! -f "$PICOCLAW_SRC/go.mod" ]]; then
echo "ERROR: PICOCLAW_SRC does not look like a Picoclaw Go module: $PICOCLAW_SRC" >&2
exit 1
fi
if [[ ! -d "$SKILL_SRC" ]]; then
echo "ERROR: SKILL_SRC not found: $SKILL_SRC" >&2
exit 1
fi
echo "[sandbox] image=$IMAGE"
echo "[sandbox] picoclaw-src=$PICOCLAW_SRC"
echo "[sandbox] skill-src=$SKILL_SRC"
echo "[sandbox] skill-version=$SKILL_VERSION"
docker run --rm \
-e HOME=/tmp/picoclaw-user-home \
-e PICOCLAW_HOME=/tmp/picoclaw-instance-home \
-e PICOCLAW_WORKSPACE=/tmp/picoclaw-workspace \
-e SKILL_VERSION="$SKILL_VERSION" \
-e CLAWHUB_PORT="$CLAWHUB_PORT" \
-v "$PICOCLAW_SRC":/opt/picoclaw-src:ro \
-v "$SKILL_SRC":/opt/skill-src:ro \
"$IMAGE" bash -lc '
set -euo pipefail
export PATH="/usr/local/go/bin:$PATH"
export DEBIAN_FRONTEND=noninteractive
apt-get update >/dev/null
apt-get install -y --no-install-recommends ca-certificates curl nodejs npm openssl zip >/dev/null
mkdir -p "$HOME" "$PICOCLAW_HOME/security/clawsec" "$PICOCLAW_WORKSPACE" /tmp/clawhub /tmp/registry-src
echo "INSIDE_HOME=$HOME"
echo "INSIDE_PICOCLAW_HOME=$PICOCLAW_HOME"
echo "INSIDE_PICOCLAW_WORKSPACE=$PICOCLAW_WORKSPACE"
# Build a ClawHub-style archive with SKILL.md at the archive root, because
# Picoclaw extracts registry ZIPs directly into workspace/skills/<slug>/.
cp /opt/skill-src/SKILL.md /opt/skill-src/README.md /opt/skill-src/CHANGELOG.md /opt/skill-src/skill.json /tmp/registry-src/
cp -a /opt/skill-src/lib /opt/skill-src/scripts /tmp/registry-src/
(
cd /tmp/registry-src
zip -qr /tmp/clawhub/picoclaw-security-guardian.zip .
)
ZIP_SHA=$(sha256sum /tmp/clawhub/picoclaw-security-guardian.zip | awk "{print \$1}")
cat > /tmp/checksums.json <<EOF
{"files":{"picoclaw-security-guardian.zip":{"sha256":"$ZIP_SHA"}}}
EOF
openssl genpkey -algorithm ed25519 -out /tmp/release-sign.key >/dev/null 2>&1
openssl pkey -in /tmp/release-sign.key -pubout -out /tmp/signing-public.pem >/dev/null 2>&1
node - <<"NODE"
const crypto = require("node:crypto");
const fs = require("node:fs");
const privateKey = crypto.createPrivateKey(fs.readFileSync("/tmp/release-sign.key"));
const manifestBytes = fs.readFileSync("/tmp/checksums.json");
fs.writeFileSync("/tmp/checksums.json.sig", crypto.sign(null, manifestBytes, privateKey).toString("base64") + "\n");
NODE
# Release artifact verification preflight: checksum + detached Ed25519 signature.
node /opt/skill-src/scripts/verify_supply_chain.mjs \
--artifact /tmp/clawhub/picoclaw-security-guardian.zip \
--checksums /tmp/checksums.json \
--signature /tmp/checksums.json.sig \
--public-key /tmp/signing-public.pem >/tmp/release-verify.log
cat > /tmp/clawhub_server.py <<"PY"
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
SKILL = "picoclaw-security-guardian"
VERSION = os.environ["SKILL_VERSION"]
ZIP_PATH = "/tmp/clawhub/picoclaw-security-guardian.zip"
SUMMARY = "Picoclaw security posture checks: advisory awareness, config drift, and supply-chain verification."
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
return
def send_json(self, obj):
body = json.dumps(obj).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == "/api/v1/search":
self.send_json({"results": [{"score": 1.0, "slug": SKILL, "displayName": "Picoclaw Security Guardian", "summary": SUMMARY, "version": VERSION}]})
return
if parsed.path == f"/api/v1/skills/{SKILL}":
self.send_json({"slug": SKILL, "displayName": "Picoclaw Security Guardian", "summary": SUMMARY, "latestVersion": {"version": VERSION}, "moderation": {"isMalwareBlocked": False, "isSuspicious": False}})
return
if parsed.path == "/api/v1/download":
qs = parse_qs(parsed.query)
if qs.get("slug", [""])[0] != SKILL:
self.send_error(404, "unknown skill")
return
data = open(ZIP_PATH, "rb").read()
self.send_response(200)
self.send_header("Content-Type", "application/zip")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
self.send_error(404, "not found")
ThreadingHTTPServer(("127.0.0.1", int(os.environ["CLAWHUB_PORT"])), Handler).serve_forever()
PY
python3 /tmp/clawhub_server.py >/tmp/clawhub.log 2>&1 &
SERVER_PID=$!
trap "kill $SERVER_PID >/dev/null 2>&1 || true; wait $SERVER_PID 2>/dev/null || true" EXIT
REGISTRY_READY=0
for _ in $(seq 1 30); do
if curl -fsS "http://127.0.0.1:$CLAWHUB_PORT/api/v1/skills/picoclaw-security-guardian" >/dev/null; then
REGISTRY_READY=1
break
fi
sleep 0.2
done
if [ "$REGISTRY_READY" -ne 1 ]; then
echo "ERROR: local ClawHub-compatible registry did not become ready" >&2
cat /tmp/clawhub.log >&2 || true
exit 1
fi
# Exercise Picoclaw itself: registry search -> install_skill -> skill loader.
cat > /tmp/picoclaw_skill_harness.go <<"GO"
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/sipeed/picoclaw/pkg/skills"
integrationtools "github.com/sipeed/picoclaw/pkg/tools/integration"
)
func must(ok bool, msg string, args ...any) {
if !ok {
fmt.Fprintf(os.Stderr, msg+"\n", args...)
os.Exit(1)
}
}
func main() {
workspace := os.Getenv("PICOCLAW_WORKSPACE")
baseURL := "http://127.0.0.1:" + os.Getenv("CLAWHUB_PORT")
version := os.Getenv("SKILL_VERSION")
registryMgr := skills.NewRegistryManager()
registryMgr.AddRegistry(skills.NewClawHubRegistry(skills.ClawHubConfig{Enabled: true, BaseURL: baseURL, Timeout: 10}))
findTool := integrationtools.NewFindSkillsTool(registryMgr, skills.NewSearchCache(50, 5*time.Minute))
findResult := findTool.Execute(context.Background(), map[string]any{"query": "picoclaw security", "limit": float64(5)})
fmt.Println(findResult.ForLLM)
must(!findResult.IsError, "find_skills failed: %s", findResult.ForLLM)
must(strings.Contains(findResult.ForLLM, "picoclaw-security-guardian"), "find_skills did not return picoclaw-security-guardian")
installTool := integrationtools.NewInstallSkillTool(registryMgr, workspace)
installResult := installTool.Execute(context.Background(), map[string]any{
"slug": "picoclaw-security-guardian",
"registry": "clawhub",
"version": version,
})
fmt.Println(installResult.ForLLM)
must(!installResult.IsError, "install_skill failed: %s", installResult.ForLLM)
must(strings.Contains(installResult.ForLLM, "Successfully installed skill"), "install_skill did not report success")
installed := filepath.Join(workspace, "skills", "picoclaw-security-guardian")
for _, rel := range []string{"SKILL.md", "skill.json", "scripts/generate_profile.mjs", "scripts/check_drift.mjs", "scripts/check_advisories.mjs", "scripts/verify_supply_chain.mjs"} {
if _, err := os.Stat(filepath.Join(installed, rel)); err != nil {
fmt.Fprintf(os.Stderr, "missing installed file %s: %v\n", rel, err)
os.Exit(1)
}
}
loader := skills.NewSkillsLoader(workspace, filepath.Join(os.Getenv("PICOCLAW_HOME"), "skills"), "")
found := false
for _, skill := range loader.ListSkills() {
if skill.Name == "picoclaw-security-guardian" && skill.Source == "workspace" {
found = true
break
}
}
must(found, "Picoclaw SkillsLoader did not list installed picoclaw-security-guardian workspace skill")
content, ok := loader.LoadSkill("picoclaw-security-guardian")
must(ok, "Picoclaw SkillsLoader could not load installed skill content")
must(strings.Contains(content, "Picoclaw Security Guardian"), "loaded skill content is not Picoclaw Security Guardian")
fmt.Println("picoclaw_find_skill=PASS")
fmt.Println("picoclaw_install_skill=PASS")
fmt.Println("picoclaw_skill_loader=PASS")
}
GO
(
cd /opt/picoclaw-src
go run /tmp/picoclaw_skill_harness.go >/tmp/picoclaw-install.log
)
cat /tmp/picoclaw-install.log
SKILL_DIR="$PICOCLAW_WORKSPACE/skills/picoclaw-security-guardian"
# Use Picoclaw-native config paths and shapes: config.json + launcher-config.json.
cat > "$PICOCLAW_HOME/config.json" <<EOF
{
"version": 3,
"agents": {
"defaults": {
"workspace": "$PICOCLAW_WORKSPACE",
"restrict_to_workspace": true,
"model_name": "sandbox-model"
}
},
"tools": {
"exec": {"enabled": false},
"cron": {"enabled": false},
"find_skills": {"enabled": true},
"install_skill": {"enabled": true}
}
}
EOF
cat > "$PICOCLAW_HOME/launcher-config.json" <<EOF
{
"port": 18800,
"public": false,
"allowed_cidrs": ["127.0.0.1/32"],
"dashboard_password_hash": "argon2id-test-hash"
}
EOF
node "$SKILL_DIR/scripts/generate_profile.mjs" \
--home "$PICOCLAW_HOME" \
--output "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" \
--generated-at 2026-04-25T00:00:00.000Z >/tmp/profile-baseline.log
cp "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" "$PICOCLAW_HOME/security/clawsec/current-profile.json"
node "$SKILL_DIR/scripts/check_drift.mjs" \
--baseline "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" \
--current "$PICOCLAW_HOME/security/clawsec/current-profile.json" \
--fail-on critical >/tmp/drift-clean.log
cat > "$PICOCLAW_HOME/config.json" <<EOF
{
"version": 3,
"agents": {
"defaults": {
"workspace": "/",
"restrict_to_workspace": false,
"allow_read_outside_workspace": true,
"model_name": "sandbox-model"
}
},
"tools": {
"exec": {"enabled": true, "allow_remote": true},
"cron": {"enabled": true, "allow_command": true},
"mcp": {
"enabled": true,
"servers": {
"dangerous-local": {"command": "node", "args": ["server.js"]}
}
},
"web": {"brave": {"enabled": true, "api_keys": ["test-secret-value"]}}
}
}
EOF
cat > "$PICOCLAW_HOME/launcher-config.json" <<EOF
{
"port": 18800,
"public": true,
"allowed_cidrs": ["0.0.0.0/0"],
"dashboard_password_hash": ""
}
EOF
node "$SKILL_DIR/scripts/generate_profile.mjs" \
--home "$PICOCLAW_HOME" \
--output "$PICOCLAW_HOME/security/clawsec/current-profile.json" \
--generated-at 2026-04-25T00:10:00.000Z >/tmp/profile-current.log
set +e
DRIFT_OUT=$(node "$SKILL_DIR/scripts/check_drift.mjs" \
--baseline "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" \
--current "$PICOCLAW_HOME/security/clawsec/current-profile.json" \
--fail-on critical 2>&1)
DRIFT_CODE=$?
set -e
[ "$DRIFT_CODE" -ne 0 ]
echo "$DRIFT_OUT" | grep -Eq "PUBLIC_WEB_UI_ENABLED|WEB_UI_AUTH_DISABLED|WORKSPACE_RESTRICTION_DISABLED"
cat > /tmp/picoclaw-feed.json <<EOF
{"version":"1.0.0","updated":"2026-04-25T00:00:00Z","advisories":[{"id":"CLAW-PICO-TEST","severity":"high","type":"prompt_injection","platforms":["picoclaw"],"affected":["picoclaw-security-guardian@$SKILL_VERSION"],"title":"Picoclaw test advisory","description":"Picoclaw gateway review","published":"2026-04-25T00:00:00Z","action":"Review before release"}]}
EOF
cat > /tmp/feed-state-unknown.json <<EOF
{"status":"unknown"}
EOF
set +e
ADVISORY_UNKNOWN_OUT=$(node "$SKILL_DIR/scripts/check_advisories.mjs" --feed /tmp/picoclaw-feed.json --state /tmp/feed-state-unknown.json 2>&1)
ADVISORY_UNKNOWN_CODE=$?
set -e
if [ "$ADVISORY_UNKNOWN_CODE" -eq 0 ]; then
echo "ERROR: advisory check unexpectedly allowed unknown feed state" >&2
exit 1
fi
echo "$ADVISORY_UNKNOWN_OUT" | grep -q "advisory feed state is not verified"
cat > /tmp/feed-state-verified.json <<EOF
{"status":"verified"}
EOF
node "$SKILL_DIR/scripts/check_advisories.mjs" --feed /tmp/picoclaw-feed.json --state /tmp/feed-state-verified.json >/tmp/advisory-verified.log
grep -q "CLAW-PICO-TEST" /tmp/advisory-verified.log
node "$SKILL_DIR/scripts/verify_supply_chain.mjs" \
--artifact /tmp/clawhub/picoclaw-security-guardian.zip \
--checksums /tmp/checksums.json \
--signature /tmp/checksums.json.sig \
--public-key /tmp/signing-public.pem >/tmp/installed-supply-chain.log
echo "=== PICOCLAW SANDBOX FEATURE TEST SUMMARY ==="
echo "picoclaw_find_skill=PASS"
echo "picoclaw_install_skill=PASS"
echo "picoclaw_skill_loader=PASS"
echo "release_verify_triad=PASS"
echo "generate_profile=PASS"
echo "picoclaw_json_config_detection=PASS"
echo "clean_drift_pass=PASS"
echo "baseline_drift_fail_closed=PASS"
echo "advisory_unknown_state_fail_closed=PASS"
echo "advisory_verified_filter=PASS"
echo "installed_supply_chain_verify=PASS"
echo "[sandbox] completed successfully"
'
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { buildPicoclawProfile, confineOutputToPicoclawHome } from "../lib/profile.mjs";
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "picoclaw-profile-"));
fs.writeFileSync(path.join(dir, "config.yaml"), "bind: 0.0.0.0\nauth: false\nmcp:\n", "utf8");
const profile = buildPicoclawProfile({ picoclawHome: dir, generatedAt: "2026-04-25T00:00:00.000Z" });
assert.equal(profile.platform, "picoclaw");
assert.equal(profile.posture.runtime.ui.public_web_ui, true);
assert.equal(profile.posture.runtime.ui.auth_disabled, true);
assert.equal(profile.posture.runtime.mcp.enabled, true);
assert.match(profile.digests.canonical_sha256, /^[a-f0-9]{64}$/);
assert.throws(() => confineOutputToPicoclawHome(path.join(dir, "..", "escape.json"), dir), /must stay under/);
console.log("profile.test.mjs PASS");
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { verifyChecksums, verifyDetachedSignature, verifySupplyChain } from "../lib/supply_chain.mjs";
import { sha256FileHex } from "../lib/profile.mjs";
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "picoclaw-supply-"));
const artifact = path.join(dir, "picoclaw");
fs.writeFileSync(artifact, "binary", "utf8");
const manifest = path.join(dir, "checksums.json");
fs.writeFileSync(manifest, JSON.stringify({ files: { picoclaw: { sha256: sha256FileHex(artifact) } } }), "utf8");
assert.equal(verifyChecksums({ artifactPath: artifact, checksumsPath: manifest }).ok, true);
assert.equal(verifySupplyChain({ artifactPath: artifact, checksumsPath: manifest }).ok, false);
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const sig = crypto.sign(null, fs.readFileSync(manifest), privateKey).toString("base64");
const pub = path.join(dir, "pub.pem");
const sigPath = path.join(dir, "checksums.json.sig");
fs.writeFileSync(pub, publicKey.export({ type: "spki", format: "pem" }));
fs.writeFileSync(sigPath, sig);
assert.equal(verifyDetachedSignature({ manifestPath: manifest, signaturePath: sigPath, publicKeyPath: pub }).ok, true);
assert.equal(verifySupplyChain({ artifactPath: artifact, checksumsPath: manifest, signaturePath: sigPath, publicKeyPath: pub }).ok, true);
console.log("supply_chain.test.mjs PASS");
Related skills
FAQ
What does picoclaw-security-guardian do?
picoclaw-security-guardian is a Claude Code skill for security.
When should I use picoclaw-security-guardian?
When you need to helps with security tasks., or when picoclaw-security-guardian is a claude code skill for security.
What are the main capabilities?
picoclaw-security-guardian; Security; AI-coding skill.