
Clawsec Clawhub Checker
- 232 installs
- 1.1k repo stars
- Updated August 4, 2026
- prompt-security/clawsec
Audit ClawHub skills and packages for risky prompts, unsafe capabilities, and policy violations before installing or publishing agent extensions.
About
Clawsec-clawhub-checker from prompt-security/clawsec provides automated security review for ClawHub agent skills, flagging risky prompts, excessive permissions, and policy issues so teams can safely ship extensions and CLI-distributed tooling through formal ship-phase security checks.
- Scans ClawHub skills for prompt-injection and unsafe patterns
- Supports pre-install and pre-publish security gates
- Aligns agent tooling with audit and compliance needs
- Targets extension and CLI skill distribution surfaces
- Reduces supply-chain risk in agent skill ecosystems
Clawsec Clawhub Checker by the numbers
- 232 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #717 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 clawsec-clawhub-checkerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 232 |
|---|---|
| repo stars | ★ 1.1k |
| Last updated | August 4, 2026 |
| Repository | prompt-security/clawsec ↗ |
What it does
Audit ClawHub skills and packages for risky prompts, unsafe capabilities, and policy violations before installing or publishing agent extensions.
Files
ClawSec ClawHub Checker
Adds a reputation gate on top of the clawsec-suite guarded installer.
Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
npx skills add prompt-security/clawsec --skill clawsec-clawhub-checker -a openclaw -yOperational Notes
- Required runtime:
node,clawhub,openclaw - Depends on: installed
clawsec-suite - Side effects: none on other skills; this package does not rewrite installed suite files
- Advisory-hook wiring is optional and manual in this release
- Network behavior: reputation checks call ClawHub inspect/search endpoints
- Trust model: scores are heuristic and confirmation-gated
What It Does
1. Reads skill metadata from ClawHub (inspect --json) 2. Evaluates scanner status (including VirusTotal summary when present) 3. Applies additional reputation heuristics (age, updates, author history, downloads) 4. Requires explicit --confirm-reputation when score is below threshold
Installation
Install after clawsec-suite:
npx clawhub@latest install clawsec-suite
npx clawhub@latest install clawsec-clawhub-checkerOptional preflight check (validates local paths and prints recommended command):
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjsRelease 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="clawsec-clawhub-checker"
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.
Usage
Run the enhanced installer directly from this skill:
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
--skill some-skill \
--version 1.0.0If a skill is below threshold, rerun only with explicit approval:
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
--skill some-skill \
--version 1.0.0 \
--confirm-reputationOptional Advisory-Hook Wiring (Manual)
This release does not auto-patch clawsec-suite hook files. If you rely on advisory alerts that include reputationWarning / reputationWarnings, wire the checker module manually:
- Source module:
~/.openclaw/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs - Target hook file:
~/.openclaw/skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts
Treat that wiring as a deliberate local customization and review it before enabling.
Exit Codes
0safe to install42advisory confirmation required (from clawsec-suite)43reputation confirmation required1error
Configuration
Environment variables:
CLAWHUB_REPUTATION_THRESHOLD- Minimum score (0-100, default: 70)
Safety Notes
- This is defense-in-depth, not a replacement for advisory matching
- Scanner outputs can produce false positives and false negatives
- Always review skill code before overriding warnings
Development
Key files:
scripts/enhanced_guarded_install.mjsscripts/check_clawhub_reputation.mjsscripts/setup_reputation_hook.mjshooks/clawsec-advisory-guardian/lib/reputation.mjs
License
GNU AGPL v3.0 or later - Part of the ClawSec security suite
test/
Changelog
[0.0.6] - 2026-06-10
Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
[0.0.5] - 2026-06-07
Security
- Treat explicit malicious ClawHub and VirusTotal verdicts as blocking signals regardless of the numeric reputation score.
[0.0.4] - 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.
All notable changes to the ClawSec ClawHub Checker will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.0.3] - 2026-04-16
Changed
- Converted setup flow to non-mutating preflight validation; the skill no longer rewrites or copies files into installed
clawsec-suitedirectories. - Updated reputation collection to rely on
clawhub inspect --jsonsecurity metadata instead of probingclawhub installoutput. - Updated documentation and metadata to describe standalone wrapper usage for guarded install checks.
- Added explicit documentation for optional manual advisory-hook wiring when operators want
reputationWarningfields in advisory alert rendering.
Security
- Removed in-place cross-skill source mutation behavior from setup.
- Removed install-output scraping behavior used only to infer VirusTotal status.
- Reputation scoring now fails closed when scanner metadata is missing, and hook-level reputation subprocess execution failures are treated as unsafe results.
[0.0.2] - 2026-04-14
Added
- Runtime and operator-review metadata describing the suite dependency, ClawHub lookups, and in-place integration behavior.
- Preflight disclosure in
scripts/setup_reputation_hook.mjsbefore the installed suite is modified. - Regression coverage for setup disclosure in
test/setup_reputation_hook.test.mjs.
Changed
- Declared
nodeandopenclawas required runtimes alongsideclawhubbecause the integration flow depends on all three. - Documented that setup rewrites installed
clawsec-suitefiles rather than operating on a detached copy.
Security
- Made the string-based
handler.tsrewrite and the remote ClawHub reputation-query behavior explicit so operators can review the mutation and network trust model before enabling it.
import { spawnSync as runProcessSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import path from "node:path";
/**
* Check reputation for a skill
* @param {string} skillName - Skill name
* @param {string} version - Skill version
* @returns {Promise<{safe: boolean, score: number, warnings: string[]}>}
*/
export async function checkReputation(skillName, version) {
const result = {
safe: true,
score: 100,
warnings: [],
};
try {
// Try to get skill slug from directory name or skill.json
// For now, use skillName as slug (simplified)
const skillSlug = skillName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
// Run the reputation check script
// Current file is at: .../hooks/clawsec-advisory-guardian/lib/reputation.mjs
// We need to go up 3 levels to get to the skill root directory
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const checkerDir = path.resolve(__dirname, '../../..');
const reputationCheck = runProcessSync(
"node",
[
`${checkerDir}/scripts/check_clawhub_reputation.mjs`,
skillSlug,
version || "",
"70" // Default threshold
],
{ encoding: "utf-8", cwd: checkerDir }
);
if (reputationCheck.error) {
result.safe = false;
result.score = 0;
result.warnings.push(`Reputation check execution error: ${reputationCheck.error.message}`);
return result;
}
if (typeof reputationCheck.status !== "number") {
result.safe = false;
result.score = 0;
result.warnings.push("Reputation check did not return a process exit status");
return result;
}
if (reputationCheck.status === 0) {
try {
const repResult = JSON.parse(reputationCheck.stdout);
result.safe = repResult.safe;
result.score = repResult.score;
result.warnings = repResult.warnings;
} catch (parseError) {
result.warnings.push(`Failed to parse reputation result: ${parseError.message}`);
result.score = 60;
result.safe = result.score >= 70;
}
} else if (reputationCheck.status === 43) {
// Reputation warning exit code
try {
const repResult = JSON.parse(reputationCheck.stdout);
result.safe = false;
result.score = repResult.score;
result.warnings = repResult.warnings;
} catch {
result.safe = false;
result.score = 50;
result.warnings.push("Skill flagged by reputation check");
}
} else {
const stderr = (reputationCheck.stderr || "").trim();
const stdout = (reputationCheck.stdout || "").trim();
const output = [stderr, stdout].filter((entry) => entry).join(" | ");
result.warnings.push(
`Reputation check failed with exit code ${reputationCheck.status}${
output ? `: ${output}` : ""
}`,
);
result.score = 0;
result.safe = false;
}
} catch (error) {
result.warnings.push(`Reputation check error: ${error.message}`);
result.score = 50;
result.safe = result.score >= 70;
}
return result;
}
/**
* Format reputation warning for alert messages
* @param {{score: number, warnings: string[]}} reputationInfo
* @returns {string}
*/
export function formatReputationWarning(reputationInfo) {
if (!reputationInfo || reputationInfo.score >= 70) return "";
const lines = [
`\n⚠️ **REPUTATION WARNING** (Score: ${reputationInfo.score}/100)`,
];
if (reputationInfo.warnings.length > 0) {
lines.push("");
reputationInfo.warnings.forEach(w => lines.push(`• ${w}`));
}
lines.push("");
lines.push("This skill has low reputation score. Review carefully before installation.");
return lines.join("\n");
}
ClawSec ClawHub Checker
A clawsec-suite companion skill that adds a standalone reputation gate before guarded installs.
Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
npx skills add prompt-security/clawsec --skill clawsec-clawhub-checker -a openclaw -yOperational Notes
- Required runtime:
node,clawhub,openclaw - Dependency: installed
clawsec-suite - No in-place mutation of other skills
- Advisory-hook wiring is optional and manual in this release
- Reputation checks query ClawHub metadata and remain confirmation-gated
Purpose
Adds a second risk signal before install by:
1. Reading ClawHub inspect/security metadata 2. Applying reputation heuristics (age, updates, author activity, downloads) 3. Requiring --confirm-reputation for low-score installs
Installation
npx clawhub install clawsec-suite
npx clawhub install clawsec-clawhub-checkerOptional preflight helper:
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjsUsage
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
--skill some-skill \
--version 1.0.0Override only after manual review:
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
--skill some-skill \
--version 1.0.0 \
--confirm-reputationOptional Advisory-Hook Wiring
If you need advisory alerts to include reputationWarning / reputationWarnings, wire the checker module manually into the installed suite hook:
- Source:
~/.openclaw/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs - Target:
~/.openclaw/skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts
The setup helper validates paths only and does not patch these files automatically.
Exit Codes
0safe to install42advisory confirmation required43reputation confirmation required1error
Configuration
CLAWHUB_REPUTATION_THRESHOLD(default: 70)
Security Considerations
- Reputation is heuristic, not authoritative
- False positives are possible
- Always inspect code before confirming installation
License
GNU AGPL v3.0 or later - Part of the ClawSec security suite
#!/usr/bin/env node
import { spawnSync as runProcessSync } from "node:child_process";
import path from "node:path";
import { pathToFileURL } from "node:url";
function runClawhub(args) {
return runProcessSync("clawhub", args, { encoding: "utf-8" });
}
function toPublicResult(result) {
return {
safe: result.safe,
score: result.score,
warnings: result.warnings,
virustotal: result.virustotal,
};
}
function finalizeResult(result, threshold) {
result.score = Math.max(0, Math.min(100, result.score));
result.safe = !result.blocked && result.score >= threshold;
if (!result.safe) {
const thresholdWarning = `Reputation score ${result.score}/100 below threshold ${threshold}/100`;
if (!result.warnings.includes(thresholdWarning)) {
result.warnings.unshift(thresholdWarning);
}
}
return toPublicResult(result);
}
function blockOnMissingScannerData(result, warning) {
result.warnings.push(warning);
result.score = Math.min(result.score, 60);
result.blocked = true;
}
function blockOnMaliciousScannerData(result, warning) {
result.warnings.push(warning);
result.score = 0;
result.blocked = true;
}
function parseJson(raw, label, warnings) {
try {
return JSON.parse(raw);
} catch (error) {
warnings.push(
`Failed to parse ${label}: ${error instanceof Error ? error.message : String(error)}`,
);
return null;
}
}
function maybeApplyVersionSecuritySignals(result, versionDetails) {
if (!versionDetails || typeof versionDetails !== "object") {
blockOnMissingScannerData(result, "ClawHub version security details are unavailable");
return;
}
const security = versionDetails.security;
if (!security || typeof security !== "object") {
blockOnMissingScannerData(result, "ClawHub version record does not include security scanner output");
return;
}
const securityStatus = typeof security.status === "string" ? security.status.toLowerCase() : "";
if (securityStatus === "malicious") {
blockOnMaliciousScannerData(result, "ClawHub static moderation marked the version as malicious");
} else if (securityStatus === "suspicious") {
result.warnings.push("ClawHub static moderation marked the version as suspicious");
result.score -= 30;
}
const scanners = security.scanners;
if (!scanners || typeof scanners !== "object") {
blockOnMissingScannerData(result, "ClawHub scanner breakdown is missing from version metadata");
return;
}
const vt = scanners.vt;
if (!vt || typeof vt !== "object") {
blockOnMissingScannerData(result, "VirusTotal scanner data was not returned by ClawHub");
return;
}
const vtStatus =
(typeof vt.normalizedStatus === "string" && vt.normalizedStatus) ||
(typeof vt.status === "string" && vt.status) ||
(typeof vt.verdict === "string" && vt.verdict) ||
"";
const normalizedStatus = vtStatus.toLowerCase();
if (normalizedStatus === "malicious") {
result.virustotal.push("ClawHub VirusTotal scan returned malicious");
blockOnMaliciousScannerData(result, "ClawHub VirusTotal scan returned malicious");
const vtSummary = typeof vt.analysis === "string" ? vt.analysis.trim() : "";
if (vtSummary) {
result.virustotal.push(vtSummary.split("\n")[0]);
}
} else if (normalizedStatus === "suspicious") {
result.virustotal.push("ClawHub VirusTotal scan returned suspicious");
result.score -= 40;
const vtSummary = typeof vt.analysis === "string" ? vt.analysis.trim() : "";
if (vtSummary) {
result.virustotal.push(vtSummary.split("\n")[0]);
}
} else if (normalizedStatus === "clean" || normalizedStatus === "benign") {
result.virustotal.push("ClawHub VirusTotal scan returned clean");
} else if (normalizedStatus) {
result.warnings.push(`VirusTotal scanner status reported as: ${normalizedStatus}`);
result.score -= 10;
} else {
result.warnings.push("VirusTotal scanner status was unavailable");
result.score -= 10;
}
}
/**
* Check ClawHub reputation for a skill
* @param {string} skillSlug - Skill slug to check
* @param {string} version - Optional version
* @param {number} threshold - Minimum reputation score (0-100)
* @returns {Promise<{safe: boolean, score: number, warnings: string[], virustotal: string[]}>}
*/
export async function checkClawhubReputation(skillSlug, version, threshold = 70) {
const result = {
safe: true,
score: 100,
warnings: [],
virustotal: [],
blocked: false,
};
if (!/^[a-z0-9][a-z0-9-]*$/.test(skillSlug)) {
result.warnings.push(`Invalid skill slug: ${skillSlug}`);
result.score = 0;
result.safe = false;
result.blocked = true;
return toPublicResult(result);
}
if (version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/.test(version)) {
result.warnings.push(`Invalid version format: ${version}`);
result.score = 0;
result.safe = false;
result.blocked = true;
return toPublicResult(result);
}
try {
const inspectArgs = ["inspect", skillSlug, "--json"];
if (version) inspectArgs.push("--version", version);
const inspectResult = runClawhub(inspectArgs);
if (inspectResult.status !== 0) {
result.warnings.push(`Skill "${skillSlug}" not found or cannot be inspected`);
result.score = Math.min(result.score, 40);
result.blocked = true;
return finalizeResult(result, threshold);
}
const skillInfo = parseJson(inspectResult.stdout, "skill inspection payload", result.warnings);
if (!skillInfo) {
result.score = Math.min(result.score, 40);
result.blocked = true;
return finalizeResult(result, threshold);
}
if (skillInfo.skill?.createdAt) {
const createdMs = skillInfo.skill.createdAt;
const ageDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24);
if (ageDays < 7) {
result.warnings.push(`Skill is less than 7 days old (${ageDays.toFixed(1)} days)`);
result.score -= 15;
} else if (ageDays < 30) {
result.warnings.push(`Skill is less than 30 days old (${ageDays.toFixed(1)} days)`);
result.score -= 5;
}
}
if (skillInfo.skill?.updatedAt && skillInfo.skill?.createdAt) {
const updatedMs = skillInfo.skill.updatedAt;
const createdMs = skillInfo.skill.createdAt;
const updateAgeDays = (Date.now() - updatedMs) / (1000 * 60 * 60 * 24);
const totalAgeDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24);
if (updateAgeDays > 90 && totalAgeDays > 90) {
result.warnings.push(`Skill hasn't been updated in ${updateAgeDays.toFixed(0)} days`);
result.score -= 10;
}
}
if (skillInfo.owner?.handle) {
const authorResult = runClawhub(["search", skillInfo.owner.handle]);
if (authorResult.status === 0) {
const lines = authorResult.stdout
.trim()
.split("\n")
.filter((line) => line);
const skillCount = Math.max(0, lines.length - 1);
if (skillCount === 1) {
result.warnings.push(`Author "${skillInfo.owner.handle}" has only 1 published skill`);
result.score -= 10;
} else if (skillCount > 1 && skillCount < 3) {
result.warnings.push(
`Author "${skillInfo.owner.handle}" has only ${skillCount} published skills`,
);
result.score -= 5;
}
}
}
if (skillInfo.skill?.stats?.downloads !== undefined) {
const downloads = skillInfo.skill.stats.downloads;
if (downloads < 10) {
result.warnings.push(`Low download count: ${downloads}`);
result.score -= 10;
} else if (downloads < 100) {
result.warnings.push(`Moderate download count: ${downloads}`);
result.score -= 5;
}
}
let versionDetails = skillInfo.version ?? null;
if (!versionDetails && !version && skillInfo.latestVersion?.version) {
const latestVersionCheck = runClawhub([
"inspect",
skillSlug,
"--version",
String(skillInfo.latestVersion.version),
"--json",
]);
if (latestVersionCheck.status === 0) {
const latestInfo = parseJson(
latestVersionCheck.stdout,
"latest-version inspection payload",
result.warnings,
);
versionDetails = latestInfo?.version ?? null;
}
}
maybeApplyVersionSecuritySignals(result, versionDetails);
return finalizeResult(result, threshold);
} catch (error) {
result.warnings.push(`Reputation check error: ${error instanceof Error ? error.message : String(error)}`);
result.score = 50;
result.blocked = true;
return finalizeResult(result, threshold);
}
}
const isCliEntrypoint =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
if (isCliEntrypoint) {
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.error("Usage: node check_clawhub_reputation.mjs <skill-slug> [version] [threshold]");
process.exit(1);
}
const skillSlug = args[0];
const version = args[1] || "";
let threshold = 70;
if (args[2] !== undefined) {
const parsedThreshold = parseInt(args[2], 10);
if (!Number.isInteger(parsedThreshold) || parsedThreshold < 0 || parsedThreshold > 100) {
console.error(
`Invalid threshold: "${args[2]}". Threshold must be an integer between 0 and 100.`,
);
process.exit(1);
}
threshold = parsedThreshold;
}
const result = await checkClawhubReputation(skillSlug, version, threshold);
console.log(JSON.stringify(result, null, 2));
if (!result.safe) {
process.exit(43);
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
#!/usr/bin/env node
import { spawnSync as runProcessSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { checkClawhubReputation } from "./check_clawhub_reputation.mjs";
const EXIT_ADVISORY_CONFIRM_REQUIRED = 42;
const EXIT_REPUTATION_CONFIRM_REQUIRED = 43;
function printUsage() {
process.stderr.write(
[
"Usage:",
" node scripts/enhanced_guarded_install.mjs --skill <skill-name> [--version <version>] [--confirm-advisory] [--confirm-reputation] [--dry-run] [--reputation-threshold <score>]",
"",
"Examples:",
" node scripts/enhanced_guarded_install.mjs --skill helper-plus --version 1.0.1",
" node scripts/enhanced_guarded_install.mjs --skill helper-plus --version 1.0.1 --confirm-advisory --confirm-reputation",
" node scripts/enhanced_guarded_install.mjs --skill suspicious-skill --reputation-threshold 80",
"",
"Exit codes:",
" 0 success / no advisory or reputation block",
" 42 advisory matched and second confirmation is required",
" 43 reputation warning and second confirmation is required",
" 1 error",
"",
].join("\n"),
);
}
function parseArgs(argv) {
// Parse and validate CLAWHUB_REPUTATION_THRESHOLD environment variable
let defaultThreshold = 70;
const envThreshold = process.env.CLAWHUB_REPUTATION_THRESHOLD;
if (envThreshold !== undefined && envThreshold !== "") {
const parsedEnv = parseInt(envThreshold, 10);
if (Number.isNaN(parsedEnv) || parsedEnv < 0 || parsedEnv > 100) {
throw new Error(
`Invalid CLAWHUB_REPUTATION_THRESHOLD environment variable: "${envThreshold}". Must be between 0 and 100.`
);
}
defaultThreshold = parsedEnv;
}
const parsed = {
skill: "",
version: "",
confirmAdvisory: false,
confirmReputation: false,
dryRun: false,
reputationThreshold: defaultThreshold,
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--skill") {
parsed.skill = String(argv[i + 1] ?? "").trim();
i += 1;
continue;
}
if (token === "--version") {
parsed.version = String(argv[i + 1] ?? "").trim();
i += 1;
continue;
}
if (token === "--confirm-advisory") {
parsed.confirmAdvisory = true;
continue;
}
if (token === "--confirm-reputation") {
parsed.confirmReputation = true;
continue;
}
if (token === "--dry-run") {
parsed.dryRun = true;
continue;
}
if (token === "--reputation-threshold") {
parsed.reputationThreshold = parseInt(String(argv[i + 1] ?? "70"), 10);
i += 1;
continue;
}
if (token === "--help" || token === "-h") {
printUsage();
process.exit(0);
}
throw new Error(`Unknown argument: ${token}`);
}
if (!parsed.skill) {
throw new Error("Missing required argument: --skill");
}
// Must start with alphanumeric, then can contain hyphens (matches check_clawhub_reputation.mjs validation)
if (!/^[a-z0-9][a-z0-9-]*$/.test(parsed.skill)) {
throw new Error("Invalid --skill value. Must start with a letter or digit, followed by lowercase letters, digits, and hyphens.");
}
if (parsed.version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/.test(parsed.version)) {
throw new Error(
"Invalid --version value. Must be semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.45)."
);
}
if (parsed.reputationThreshold < 0 || parsed.reputationThreshold > 100 || Number.isNaN(parsed.reputationThreshold)) {
throw new Error("Invalid --reputation-threshold value. Must be between 0 and 100.");
}
return parsed;
}
function buildOriginalArgs(argv) {
// Filter out reputation-specific arguments that the original script doesn't understand
const originalArgs = [];
for (let i = 0; i < argv.length; i++) {
const token = argv[i];
if (token === "--confirm-reputation" || token === "--reputation-threshold") {
// Skip reputation-specific flags
if (token === "--reputation-threshold" && i + 1 < argv.length) {
// Also skip the value associated with --reputation-threshold
i += 1;
}
continue;
}
originalArgs.push(token);
}
return originalArgs;
}
async function runOriginalGuardedInstall(args) {
// Find the original guarded_skill_install.mjs from clawsec-suite
const suiteDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-suite");
const originalScript = path.join(suiteDir, "scripts", "guarded_skill_install.mjs");
try {
await fs.access(originalScript);
} catch {
throw new Error(`Original guarded_skill_install.mjs not found at ${originalScript}. Is clawsec-suite installed?`);
}
// Pass through environment without modification
// The original guarded_skill_install.mjs handles --confirm-advisory properly
const child = runProcessSync(
"node",
[originalScript, ...args.originalArgs],
{
stdio: "inherit",
env: process.env,
cwd: suiteDir,
},
);
return {
exitCode: child.status ?? 1,
signal: child.signal,
};
}
async function main() {
try {
const cliArgs = process.argv.slice(2);
const args = parseArgs(cliArgs);
// Build args for original script (excluding reputation-specific args)
args.originalArgs = buildOriginalArgs(cliArgs);
// Step 1: Check reputation (unless already confirmed)
if (!args.confirmReputation) {
console.log(`Checking ClawHub reputation for ${args.skill}${args.version ? `@${args.version}` : ""}...`);
const reputationResult = await checkClawhubReputation(args.skill, args.version, args.reputationThreshold);
if (!reputationResult.safe) {
console.error("\n" + "=".repeat(80));
console.error("REPUTATION WARNING");
console.error("=".repeat(80));
console.error(`Skill "${args.skill}" has low reputation score: ${reputationResult.score}/100`);
console.error(`Threshold: ${args.reputationThreshold}/100`);
console.error("");
if (reputationResult.warnings.length > 0) {
console.error("Warnings:");
reputationResult.warnings.forEach(w => console.error(` • ${w}`));
console.error("");
}
if (reputationResult.virustotal) {
console.error("VirusTotal Code Insight flags:");
reputationResult.virustotal.forEach(v => console.error(` • ${v}`));
console.error("");
}
console.error("To install despite reputation warning, run with --confirm-reputation flag:");
console.error(` node ${process.argv[1]} --skill ${args.skill}${args.version ? ` --version ${args.version}` : ""} --confirm-reputation`);
console.error("");
console.error("=".repeat(80));
process.exit(EXIT_REPUTATION_CONFIRM_REQUIRED);
}
console.log(`✓ Reputation check passed: ${reputationResult.score}/100`);
} else {
console.log(`⚠️ Reputation confirmation override enabled for ${args.skill}`);
}
// Step 2: Run original guarded installer (handles advisory checks)
console.log("\nRunning advisory checks...");
const result = await runOriginalGuardedInstall(args);
if (result.exitCode !== 0 && result.exitCode !== EXIT_ADVISORY_CONFIRM_REQUIRED) {
process.exit(result.exitCode);
}
// If we get here, either success (0) or advisory confirmation required (42)
process.exit(result.exitCode);
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}
}
main();
#!/usr/bin/env node
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
function printUsage() {
console.log([
"Usage:",
" node scripts/setup_reputation_hook.mjs",
"",
"This helper no longer mutates installed clawsec-suite files.",
"It validates local prerequisites and prints the standalone checker command.",
"",
].join("\n"));
}
function printSummary({ suiteDir, checkerDir, enhancedInstaller }) {
const lines = [
"Preflight review:",
"- This setup does not rewrite files in other skills.",
`- It validates expected install paths: ${suiteDir} and ${checkerDir}.`,
"- Required runtime for reputation checks: node + clawhub.",
"- Advisory-hook reputation annotations are manual only in this release.",
"- If you want hook alert annotations, wire checker lib/reputation.mjs into suite handler.ts yourself.",
"- Reputation scoring is heuristic and must remain confirmation-gated.",
"",
"Recommended command:",
` node ${enhancedInstaller} --skill <slug> [--version <semver>]`,
"",
"Optional shell alias (manual, not applied automatically):",
` alias clawsec-guarded-install='node ${enhancedInstaller}'`,
];
console.log(lines.join("\n"));
}
async function main() {
if (process.argv.includes("--help") || process.argv.includes("-h")) {
printUsage();
return;
}
const suiteDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-suite");
const checkerDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-clawhub-checker");
const enhancedInstaller = path.join(checkerDir, "scripts", "enhanced_guarded_install.mjs");
const suiteGuardedInstaller = path.join(suiteDir, "scripts", "guarded_skill_install.mjs");
await fs.access(checkerDir);
await fs.access(enhancedInstaller);
await fs.access(suiteDir);
await fs.access(suiteGuardedInstaller);
printSummary({ suiteDir, checkerDir, enhancedInstaller });
}
main().catch((error) => {
console.error(`Setup failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
{
"name": "clawsec-clawhub-checker",
"version": "0.0.6",
"description": "ClawHub reputation checker for clawsec-suite. Adds a standalone reputation gate before guarded skill installation.",
"author": "abutbul",
"license": "AGPL-3.0-or-later",
"homepage": "https://clawsec.prompt.security/",
"keywords": [
"security",
"reputation",
"clawhub",
"virustotal",
"skills",
"installer",
"verification",
"defense-in-depth",
"openclaw"
],
"sbom": {
"files": [
{
"path": "SKILL.md",
"required": true,
"description": "Skill documentation and usage guide"
},
{
"path": "scripts/enhanced_guarded_install.mjs",
"required": true,
"description": "Enhanced guarded installer with reputation checks"
},
{
"path": "scripts/check_clawhub_reputation.mjs",
"required": true,
"description": "ClawHub reputation checking logic"
},
{
"path": "scripts/setup_reputation_hook.mjs",
"required": true,
"description": "Non-mutating preflight helper that validates paths and prints recommended commands"
},
{
"path": "hooks/clawsec-advisory-guardian/lib/reputation.mjs",
"required": false,
"description": "Optional reputation module for advisory guardian integrations"
},
{
"path": "README.md",
"required": false,
"description": "Additional documentation and development guide"
},
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history and release notes"
}
]
},
"dependencies": {
"clawsec-suite": ">=0.0.10"
},
"integration": {
"clawsec-suite": {
"enhances": [
"guarded_skill_install.mjs via external wrapper invocation",
"optional manual advisory-guardian hook wiring for reputation annotations"
],
"adds_exit_codes": {
"43": "Reputation warning - requires --confirm-reputation"
},
"adds_arguments": [
"--confirm-reputation",
"--reputation-threshold"
]
}
},
"openclaw": {
"emoji": "🛡️",
"category": "security",
"requires": {
"bins": [
"node",
"clawhub",
"openclaw"
]
},
"runtime": {
"required_env": [],
"optional_env": [
"CLAWHUB_REPUTATION_THRESHOLD"
]
},
"execution": {
"always": false,
"persistence": "No automatic persistence; setup helper performs validation only and does not rewrite other skills.",
"network_egress": "Reputation checks query ClawHub inspect/search endpoints for metadata and scanner summaries."
},
"operator_review": [
"Requires an installed clawsec-suite checkout because the enhanced installer delegates to suite guarded install flow.",
"This release does not auto-wire advisory-guardian hook annotations; if needed, wire hooks/clawsec-advisory-guardian/lib/reputation.mjs manually into the suite hook.",
"Reputation results are heuristic and can produce false positives; installation still requires explicit user confirmation for risky skills.",
"Run the setup helper to confirm local paths before using the enhanced installer command."
],
"triggers": [
"clawhub reputation",
"skill reputation check",
"virustotal skill check",
"safe skill install",
"check skill safety",
"skill security score"
]
}
}
#!/usr/bin/env node
/**
* Reputation check tests for clawsec-clawhub-checker.
*
* Tests cover:
* - Input validation (command injection prevention)
* - Reputation scoring with mocked clawhub output
* - formatReputationWarning output formatting
* - Enhanced installer argument parsing
*
* Run: node skills/clawsec-clawhub-checker/test/reputation_check.test.mjs
*/
import { fileURLToPath } from "node:url";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CHECKER_SCRIPT = path.resolve(__dirname, "..", "scripts", "check_clawhub_reputation.mjs");
const ENHANCED_INSTALL_SCRIPT = path.resolve(__dirname, "..", "scripts", "enhanced_guarded_install.mjs");
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount++;
console.log(`\u2713 ${name}`);
}
function fail(name, error) {
failCount++;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}
function runScript(scriptPath, args, env) {
return new Promise((resolve) => {
const proc = spawn("node", [scriptPath, ...args], {
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
});
proc.on("close", (code) => {
resolve({ code, stdout, stderr });
});
});
}
async function createMockClawhub(payload) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawhub-reputation-test-"));
const binDir = path.join(tmpDir, "bin");
const mockPath = path.join(binDir, "clawhub");
await fs.mkdir(binDir, { recursive: true });
await fs.writeFile(
mockPath,
`#!/usr/bin/env node
const payload = ${JSON.stringify(JSON.stringify(payload))};
const command = process.argv[2] || "";
if (command === "inspect") {
process.stdout.write(payload);
process.exit(0);
}
if (command === "search") {
process.stdout.write("name\\nmock-skill\\nother-skill\\n");
process.exit(0);
}
process.stderr.write("unexpected clawhub command: " + process.argv.slice(2).join(" ") + "\\n");
process.exit(2);
`,
"utf8",
);
await fs.chmod(mockPath, 0o755);
return {
env: { PATH: `${binDir}:${process.env.PATH}` },
cleanup: async () => fs.rm(tmpDir, { recursive: true, force: true }),
};
}
// -----------------------------------------------------------------------------
// Test: Invalid skill slug is rejected (command injection prevention)
// -----------------------------------------------------------------------------
async function testInvalidSlugRejected() {
const testName = "reputation_check: invalid slug with shell metacharacters is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['test; rm -rf /', '', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
if (parsed.score === 0 && parsed.safe === false && parsed.warnings.some(w => w.includes("Invalid skill slug"))) {
pass(testName);
} else {
fail(testName, `Expected score 0 with invalid slug warning, got: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Invalid version format is rejected (command injection prevention)
// -----------------------------------------------------------------------------
async function testInvalidVersionRejected() {
const testName = "reputation_check: invalid version with shell metacharacters is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0; curl evil.com', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
if (parsed.score === 0 && parsed.safe === false && parsed.warnings.some(w => w.includes("Invalid version format"))) {
pass(testName);
} else {
fail(testName, `Expected score 0 with invalid version warning, got: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Valid slug and version pass input validation
// -----------------------------------------------------------------------------
async function testValidInputsAccepted() {
const testName = "reputation_check: valid slug and semver pass input validation";
try {
// clawhub is not installed, so the check will fail at the inspect step,
// but it should NOT fail at input validation
const result = await runScript(CHECKER_SCRIPT, ['my-test-skill', '1.0.0', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
// Should not contain input validation errors
const hasInputError = parsed.warnings.some(
w => w.includes("Invalid skill slug") || w.includes("Invalid version format")
);
if (!hasInputError) {
pass(testName);
} else {
fail(testName, `Valid inputs were rejected: ${JSON.stringify(parsed.warnings)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Slug with uppercase or special chars is rejected
// -----------------------------------------------------------------------------
async function testUppercaseSlugRejected() {
const testName = "reputation_check: uppercase slug is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['Test-Skill', '1.0.0', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
if (parsed.score === 0 && parsed.safe === false) {
pass(testName);
} else {
fail(testName, `Expected uppercase slug to be rejected, got: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Empty slug shows usage error
// -----------------------------------------------------------------------------
async function testEmptySlugShowsUsage() {
const testName = "reputation_check: empty slug shows usage error";
try {
const result = await runScript(CHECKER_SCRIPT, []);
if (result.code === 1 && result.stderr.includes("Usage:")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with usage message, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Version with pre-release tag is accepted
// -----------------------------------------------------------------------------
async function testPreReleaseVersionAccepted() {
const testName = "reputation_check: pre-release version format is accepted";
try {
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0-beta.1', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
const hasVersionError = parsed.warnings.some(w => w.includes("Invalid version format"));
if (!hasVersionError) {
pass(testName);
} else {
fail(testName, `Pre-release version was rejected: ${JSON.stringify(parsed.warnings)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Explicit malicious scanner verdict blocks regardless of score
// -----------------------------------------------------------------------------
async function testMaliciousVirusTotalVerdictBlocks() {
const testName = "reputation_check: malicious VirusTotal verdict blocks install";
const now = Date.now();
const mock = await createMockClawhub({
skill: {
createdAt: now - (120 * 24 * 60 * 60 * 1000),
updatedAt: now - (2 * 24 * 60 * 60 * 1000),
stats: { downloads: 1000 },
},
owner: { handle: "trusted-publisher" },
version: {
security: {
status: "clean",
scanners: {
vt: {
normalizedStatus: "malicious",
analysis: "malicious verdict from scanner",
},
},
},
},
});
try {
const result = await runScript(CHECKER_SCRIPT, ['malicious-skill', '1.0.0', '70'], mock.env);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
if (
result.code === 43 &&
parsed.safe === false &&
parsed.warnings.some((w) => w.toLowerCase().includes("malicious")) &&
parsed.virustotal.some((v) => v.toLowerCase().includes("malicious"))
) {
pass(testName);
} else {
fail(testName, `Expected malicious verdict to block, got code ${result.code}: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
} finally {
await mock.cleanup();
}
}
// -----------------------------------------------------------------------------
// Test: CLI entrypoint guard works when script path is relative
// -----------------------------------------------------------------------------
async function testRelativePathCliEntrypointWorks() {
const testName = "reputation_check: CLI entrypoint works with relative script path";
try {
const relativeCheckerScript = path.relative(process.cwd(), CHECKER_SCRIPT);
const result = await runScript(relativeCheckerScript, ['bad slug', '', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output with relative script path: ${result.stdout}`);
return;
}
if (
result.code === 43 &&
parsed.safe === false &&
parsed.warnings.some((w) => w.includes("Invalid skill slug"))
) {
pass(testName);
} else {
fail(
testName,
`Expected exit 43 with invalid slug warning via relative path, got code ${result.code}: ${JSON.stringify(parsed)}`
);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Invalid threshold format is rejected in CLI mode
// -----------------------------------------------------------------------------
async function testInvalidThresholdRejected() {
const testName = "reputation_check: invalid threshold is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0', 'abc']);
if (result.code === 1 && result.stderr.includes("Invalid threshold")) {
pass(testName);
} else {
fail(
testName,
`Expected exit 1 with invalid threshold message, got code ${result.code}: ${result.stderr}`
);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer rejects invalid skill name
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRejectsInvalidSkill() {
const testName = "enhanced_install: rejects skill name with invalid characters";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, ['--skill', 'bad skill!']);
if (result.code === 1 && result.stderr.includes("Invalid --skill value")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with invalid skill error, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer requires --skill argument
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRequiresSkill() {
const testName = "enhanced_install: requires --skill argument";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, []);
if (result.code === 1 && result.stderr.includes("Missing required argument")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with missing argument error, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer rejects invalid threshold
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRejectsInvalidThreshold() {
const testName = "enhanced_install: rejects invalid reputation threshold";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, [
'--skill', 'test-skill', '--reputation-threshold', '150'
]);
if (result.code === 1 && result.stderr.includes("Invalid --reputation-threshold")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with invalid threshold error, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: formatReputationWarning
// -----------------------------------------------------------------------------
async function testFormatReputationWarning() {
const testName = "reputation: formatReputationWarning formats correctly";
try {
const { formatReputationWarning } = await import(
path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs")
);
// Safe reputation — should return empty
const safeResult = formatReputationWarning({ score: 80, warnings: [] });
if (safeResult !== "") {
fail(testName, `Expected empty string for safe score, got: "${safeResult}"`);
return;
}
// Unsafe reputation — should contain warning
const unsafeResult = formatReputationWarning({ score: 45, warnings: ["Low downloads", "New author"] });
if (
unsafeResult.includes("REPUTATION WARNING") &&
unsafeResult.includes("45/100") &&
unsafeResult.includes("Low downloads") &&
unsafeResult.includes("New author")
) {
pass(testName);
} else {
fail(testName, `Unexpected format: "${unsafeResult}"`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: formatReputationWarning handles null/undefined
// -----------------------------------------------------------------------------
async function testFormatReputationWarningNull() {
const testName = "reputation: formatReputationWarning handles null input";
try {
const { formatReputationWarning } = await import(
path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs")
);
const nullResult = formatReputationWarning(null);
const undefinedResult = formatReputationWarning(undefined);
if (nullResult === "" && undefinedResult === "") {
pass(testName);
} else {
fail(testName, `Expected empty for null/undefined, got: "${nullResult}", "${undefinedResult}"`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer validates --version even with --confirm-reputation
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRejectsInvalidVersion() {
const testName = "enhanced_install: rejects invalid version format even with --confirm-reputation";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, [
'--skill', 'test-skill', '--version', '1.0.0;rm -rf /', '--confirm-reputation'
]);
if (result.code === 1 && result.stderr.includes("Invalid --version value")) {
pass(testName);
} else {
fail(
testName,
`Expected exit 1 with invalid version message, got code ${result.code}: ${result.stderr}`
);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Main test runner
// -----------------------------------------------------------------------------
async function runTests() {
console.log("=== ClawSec ClawHub Checker Tests ===\n");
await testInvalidSlugRejected();
await testInvalidVersionRejected();
await testValidInputsAccepted();
await testUppercaseSlugRejected();
await testEmptySlugShowsUsage();
await testPreReleaseVersionAccepted();
await testMaliciousVirusTotalVerdictBlocks();
await testRelativePathCliEntrypointWorks();
await testInvalidThresholdRejected();
await testEnhancedInstallerRejectsInvalidSkill();
await testEnhancedInstallerRequiresSkill();
await testEnhancedInstallerRejectsInvalidVersion();
await testEnhancedInstallerRejectsInvalidThreshold();
await testFormatReputationWarning();
await testFormatReputationWarningNull();
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
}
runTests().catch((error) => {
console.error("Test runner failed:", error);
process.exit(1);
});
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createTempDir, pass, fail, report, exitWithResults } from "../../clawsec-suite/test/lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const NODE_BIN = process.execPath;
const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "setup_reputation_hook.mjs");
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
async function runScript(env) {
return await new Promise((resolve) => {
const proc = spawn(NODE_BIN, [SCRIPT_PATH], {
env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
});
proc.on("close", (code) => {
resolve({ code, stdout, stderr });
});
});
}
async function stageInstalledSkill(tempHome, skillName) {
const sourceDir = path.join(REPO_ROOT, "skills", skillName);
const destDir = path.join(tempHome, ".openclaw", "skills", skillName);
await fs.mkdir(path.dirname(destDir), { recursive: true });
await fs.cp(sourceDir, destDir, { recursive: true });
return destDir;
}
async function testPreflightSummaryNoMutation() {
const testName = "setup_reputation_hook: prints preflight review without mutating installed suite files";
const tmp = await createTempDir();
const homeDir = path.join(tmp.path, "home");
try {
await stageInstalledSkill(homeDir, "clawsec-suite");
await stageInstalledSkill(homeDir, "clawsec-clawhub-checker");
const result = await runScript({
...process.env,
HOME: homeDir,
});
if (result.code !== 0) {
fail(testName, `script failed: ${result.stderr}`);
return;
}
const wrapperPath = path.join(
homeDir,
".openclaw",
"skills",
"clawsec-suite",
"scripts",
"guarded_skill_install_wrapper.mjs",
);
const reputationModulePath = path.join(
homeDir,
".openclaw",
"skills",
"clawsec-suite",
"hooks",
"clawsec-advisory-guardian",
"lib",
"reputation.mjs",
);
const wrapperExists = await fs
.access(wrapperPath)
.then(() => true)
.catch(() => false);
const reputationModuleExists = await fs
.access(reputationModulePath)
.then(() => true)
.catch(() => false);
if (
result.stdout.includes("Preflight review:") &&
result.stdout.includes("does not rewrite files in other skills") &&
result.stdout.includes("Recommended command:") &&
result.stdout.includes("alias clawsec-guarded-install") &&
wrapperExists === false &&
reputationModuleExists === false
) {
pass(testName);
} else {
fail(testName, `missing preflight detail: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
} finally {
await tmp.cleanup();
}
}
async function runAllTests() {
await testPreflightSummaryNoMutation();
report();
exitWithResults();
}
runAllTests().catch((err) => {
console.error("Test runner failed:", err);
process.exit(1);
});