🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

clawfix

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clawfix - npm Package Compare versions

Comparing version
0.8.0
to
0.9.1
+386
bin/native-diagnostics.js
import { spawnSync } from 'node:child_process';
import { homedir } from 'node:os';
import { redactText } from './security.js';
function cleanText(value, maxLength = 1000) {
return String(value || '')
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
.trim()
.slice(0, maxLength);
}
function isRecord(value) {
return value != null && typeof value === 'object' && !Array.isArray(value);
}
function hasAcceptedJsonExitCode(result) {
return result.status === 0 || result.status === 1;
}
export function redactDiagnosticText(value) {
return redactText(value);
}
function cleanPath(value) {
return redactDiagnosticText(cleanText(value, 500)).replaceAll(homedir(), '~');
}
function runJsonCommand(openclawBin, args, spawn, timeout = 30_000) {
const result = spawn(openclawBin, args, {
encoding: 'utf8',
timeout,
stdio: ['ignore', 'pipe', 'pipe'],
});
const stdout = cleanText(result.stdout, 500_000);
const stderr = redactDiagnosticText(cleanText(result.stderr || result.error?.message, 2000));
if (result.error || result.signal || !Number.isInteger(result.status)) {
return {
result,
parsed: null,
error: stderr || (result.signal ? `Command terminated by ${result.signal}` : 'Command failed before completion'),
};
}
if (!hasAcceptedJsonExitCode(result)) {
return { result, parsed: null, error: stderr || `Command exited with status ${result.status}` };
}
try {
return { result, parsed: JSON.parse(stdout), error: stderr };
} catch {
return { result, parsed: null, error: stderr || (stdout ? 'Command returned invalid JSON' : '') };
}
}
function normalizeFinding(finding, source) {
return {
checkId: cleanText(finding.checkId, 200),
source,
severity: ['info', 'warning', 'warn', 'error', 'critical'].includes(finding.severity)
? finding.severity
: 'warning',
title: redactDiagnosticText(cleanText(finding.title, 500)) || null,
message: redactDiagnosticText(cleanText(finding.message || finding.detail, 2000)),
path: cleanPath(finding.path) || null,
fixHint: redactDiagnosticText(cleanText(finding.fixHint || finding.remediation, 2000)) || null,
};
}
export function collectOpenClawVersion(openclawBin, spawn = spawnSync) {
const result = spawn(openclawBin, ['--version'], {
encoding: 'utf8',
timeout: 10_000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const version = cleanText(result.stdout, 300);
const error = redactDiagnosticText(cleanText(result.stderr || result.error?.message, 1000));
const mismatch = error.match(/Node\.js (.+?) is required \(current: ([^)]+)\)/i);
return {
version,
runtimeCompatible: result.status === 0 && !mismatch,
runtimeRequired: mismatch?.[1]?.trim() || null,
runtimeCurrent: mismatch?.[2]?.trim() || null,
error: result.status === 0 ? '' : error,
};
}
export function collectNativeDoctor(openclawBin, spawn = spawnSync) {
const result = spawn(openclawBin, [
'doctor',
'--lint',
'--json',
'--severity-min', 'warning',
'--skip', 'core/doctor/skills-readiness',
'--no-workspace-suggestions',
], {
encoding: 'utf8',
timeout: 30_000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const stdout = cleanText(result.stdout, 250_000);
if (result.error || result.signal || !Number.isInteger(result.status) || !hasAcceptedJsonExitCode(result)) {
return {
available: false,
exitCode: result.status,
error: redactDiagnosticText(cleanText(
result.stderr || result.error?.message || (result.signal ? `Doctor terminated by ${result.signal}` : `Doctor exited with status ${result.status}`),
1000,
)),
checksRun: 0,
checksSkipped: 0,
findings: [],
};
}
if (!stdout) {
return {
available: false,
exitCode: result.status,
error: redactDiagnosticText(cleanText(result.stderr || result.error?.message, 1000)),
checksRun: 0,
checksSkipped: 0,
findings: [],
};
}
try {
const parsed = JSON.parse(stdout);
if (!isRecord(parsed)
|| typeof parsed.ok !== 'boolean'
|| !Number.isSafeInteger(parsed.checksRun)
|| parsed.checksRun < 0
|| !Number.isSafeInteger(parsed.checksSkipped)
|| parsed.checksSkipped < 0
|| !Array.isArray(parsed.findings)) {
throw new TypeError('invalid Doctor envelope');
}
const findings = parsed.findings.slice(0, 100).map(finding => ({
checkId: cleanText(finding.checkId, 200),
severity: ['info', 'warning', 'error'].includes(finding.severity)
? finding.severity
: 'warning',
message: redactDiagnosticText(cleanText(finding.message, 2000)),
path: cleanPath(finding.path) || null,
fixHint: redactDiagnosticText(cleanText(finding.fixHint, 2000)) || null,
}));
return {
available: true,
exitCode: result.status,
ok: parsed.ok === true,
checksRun: Number.isSafeInteger(parsed.checksRun) ? parsed.checksRun : 0,
checksSkipped: Number.isSafeInteger(parsed.checksSkipped) ? parsed.checksSkipped : 0,
findings,
};
} catch {
return {
available: false,
exitCode: result.status,
error: 'OpenClaw Doctor returned invalid JSON',
checksRun: 0,
checksSkipped: 0,
findings: [],
};
}
}
export function collectNativeConfigValidation(openclawBin, spawn = spawnSync) {
const { result, parsed, error } = runJsonCommand(
openclawBin,
['config', 'validate', '--json'],
spawn,
20_000,
);
if (!parsed) {
return {
available: false,
exitCode: result.status,
valid: null,
warnings: [],
errors: error ? [error] : [],
};
}
const valid = typeof parsed.valid === 'boolean'
? parsed.valid
: typeof parsed.ok === 'boolean' ? parsed.ok : null;
if (!isRecord(parsed)
|| valid === null
|| (parsed.issues != null && !Array.isArray(parsed.issues))
|| (parsed.errors != null && !Array.isArray(parsed.errors))
|| (parsed.warnings != null && !Array.isArray(parsed.warnings))) {
return {
available: false,
exitCode: result.status,
valid: null,
warnings: [],
errors: ['OpenClaw config validation returned an invalid JSON envelope'],
};
}
const rawErrors = Array.isArray(parsed.issues)
? parsed.issues
: Array.isArray(parsed.errors) ? parsed.errors : [];
const rawWarnings = Array.isArray(parsed.warnings) ? parsed.warnings : [];
return {
available: true,
exitCode: result.status,
valid,
path: cleanPath(parsed.path || parsed.configPath) || null,
warnings: rawWarnings.slice(0, 50).map(item => (
redactDiagnosticText(cleanText(item?.message || item, 2000))
)),
errors: rawErrors.slice(0, 50).map(item => {
const rawPath = item?.path || item?.ref;
const path = Array.isArray(rawPath) ? rawPath.join('.') : rawPath;
return {
kind: cleanText(item?.kind || item?.code, 100) || 'schema',
path: cleanPath(path) || null,
message: redactDiagnosticText(cleanText(item?.message || item?.error || item, 2000)),
};
}),
};
}
export function collectNativeStatus(openclawBin, spawn = spawnSync) {
const { result, parsed, error } = runJsonCommand(openclawBin, ['status', '--json'], spawn, 30_000);
if (!parsed) {
return { available: false, exitCode: result.status, error };
}
if (!isRecord(parsed)
|| typeof parsed.runtimeVersion !== 'string'
|| !parsed.runtimeVersion.trim()
|| !isRecord(parsed.gateway)
|| typeof parsed.gateway.reachable !== 'boolean') {
return {
available: false,
exitCode: result.status,
error: 'OpenClaw status returned an invalid JSON envelope',
};
}
return {
available: true,
exitCode: result.status,
runtimeVersion: cleanText(parsed.runtimeVersion, 100) || null,
gateway: {
mode: cleanText(parsed.gateway?.mode, 50) || null,
reachable: typeof parsed.gateway?.reachable === 'boolean'
? parsed.gateway.reachable
: null,
misconfigured: parsed.gateway?.misconfigured === true,
connectLatencyMs: Number.isFinite(parsed.gateway?.connectLatencyMs)
? parsed.gateway.connectLatencyMs
: null,
error: redactDiagnosticText(cleanText(parsed.gateway?.error, 1000)) || null,
authWarning: redactDiagnosticText(cleanText(parsed.gateway?.authWarning, 1000)) || null,
},
gatewayService: {
label: cleanText(parsed.gatewayService?.label, 100) || null,
installed: parsed.gatewayService?.installed === true,
loaded: parsed.gatewayService?.loaded === true,
externallyManaged: parsed.gatewayService?.externallyManaged === true,
status: cleanText(parsed.gatewayService?.runtime?.status, 100) || null,
detail: redactDiagnosticText(cleanText(parsed.gatewayService?.runtime?.detail, 1000)) || null,
},
tasks: {
total: Number.isSafeInteger(parsed.tasks?.total) ? parsed.tasks.total : 0,
active: Number.isSafeInteger(parsed.tasks?.active) ? parsed.tasks.active : 0,
failures: Number.isSafeInteger(parsed.tasks?.failures) ? parsed.tasks.failures : 0,
},
secretDiagnosticCount: Array.isArray(parsed.secretDiagnostics)
? parsed.secretDiagnostics.length
: 0,
};
}
export function collectNativeSecurityAudit(openclawBin, spawn = spawnSync) {
const { result, parsed, error } = runJsonCommand(
openclawBin,
['security', 'audit', '--json'],
spawn,
30_000,
);
if (!parsed) {
return { available: false, exitCode: result.status, error, findings: [] };
}
const warningCount = parsed?.summary?.warn ?? parsed?.summary?.warning;
if (!isRecord(parsed)
|| !isRecord(parsed.summary)
|| !Number.isSafeInteger(parsed.summary.critical)
|| parsed.summary.critical < 0
|| !Number.isSafeInteger(warningCount)
|| warningCount < 0
|| !Number.isSafeInteger(parsed.summary.info)
|| parsed.summary.info < 0
|| !Array.isArray(parsed.findings)) {
return {
available: false,
exitCode: result.status,
error: 'OpenClaw security audit returned an invalid JSON envelope',
findings: [],
};
}
return {
available: true,
exitCode: result.status,
summary: {
critical: Number.isSafeInteger(parsed.summary?.critical) ? parsed.summary.critical : 0,
warning: Number.isSafeInteger(parsed.summary?.warn)
? parsed.summary.warn
: Number.isSafeInteger(parsed.summary?.warning) ? parsed.summary.warning : 0,
info: Number.isSafeInteger(parsed.summary?.info) ? parsed.summary.info : 0,
},
findings: Array.isArray(parsed.findings)
? parsed.findings.slice(0, 100).map(finding => normalizeFinding(finding, 'openclaw-security'))
: [],
suppressedFindingCount: Array.isArray(parsed.suppressedFindings)
? parsed.suppressedFindings.length
: 0,
secretDiagnosticCount: Array.isArray(parsed.secretDiagnostics)
? parsed.secretDiagnostics.length
: 0,
};
}
export function collectListeningPort(port, spawn = spawnSync) {
if (!Number.isInteger(port) || port < 1 || port > 65535) {
return {
valid: false,
listening: false,
process: null,
pid: null,
endpoint: null,
collector: null,
finding: {
checkId: 'config/gateway-port-invalid',
severity: 'error',
path: 'gateway.port',
message: `Gateway port must be an integer between 1 and 65535; received ${cleanText(port, 100) || String(port)}`,
},
};
}
const lsof = spawn('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'], {
encoding: 'utf8',
timeout: 5000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const lsofLines = cleanText(lsof.stdout, 20_000).split('\n').filter(Boolean);
if (lsof.status === 0 && lsofLines.length > 1) {
const fields = lsofLines[1].trim().split(/\s+/);
const endpoint = lsofLines[1].match(/\b(?:TCP|UDP)\s+(\S+)/)?.[1];
return {
valid: true,
listening: true,
process: cleanText(fields[0], 100) || null,
pid: Number.parseInt(fields[1], 10) || null,
endpoint: cleanText(endpoint, 200) || null,
collector: 'lsof',
};
}
const ss = spawn('ss', ['-ltnp', `sport = :${port}`], {
encoding: 'utf8',
timeout: 5000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const ssOutput = cleanText(ss.stdout, 20_000);
const processMatch = ssOutput.match(/users:\(\(\"([^\"]+)\",pid=(\d+)/);
const endpointMatch = ssOutput.match(/LISTEN\s+\d+\s+\d+\s+(\S+):\d+/);
if (ss.status === 0 && /\bLISTEN\b/.test(ssOutput)) {
return {
valid: true,
listening: true,
process: cleanText(processMatch?.[1], 100) || null,
pid: Number.parseInt(processMatch?.[2], 10) || null,
endpoint: cleanText(endpointMatch?.[1], 200) || null,
collector: 'ss',
};
}
return { valid: true, listening: false, process: null, pid: null, endpoint: null, collector: null };
}
import { homedir } from 'node:os';
export const FIX_ID_PATTERN = /^[A-Za-z0-9_-]{10,64}$/;
const REDACTED = '***REDACTED***';
const SENSITIVE_KEY = /(?:^|_)(?:api[_-]?key|access[_-]?token|auth(?:orization)?|bearer|cookie|credential|jwt|password|private[_-]?key|secret|session[_-]?token|token)(?:$|_)/i;
const ISSUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/;
function normalizeKey(value) {
return String(value)
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.replace(/[^A-Za-z0-9]+/g, '_')
.toLowerCase();
}
function isSensitiveKey(value) {
return SENSITIVE_KEY.test(normalizeKey(value));
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function redactText(value, { home = homedir() } = {}) {
let text = String(value ?? '');
text = text
.replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/gi, REDACTED)
.replace(/(Authorization\s*:\s*Bearer\s+)[^\s,;]+/gi, `$1${REDACTED}`)
.replace(/\b((?:https?|postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\/)([^\s/@:]+):([^\s/@]+)@/gi, `$1${REDACTED}:${REDACTED}@`)
.replace(/\b(?:sk(?:-or-v\d+)?[-_]|xai[-_]|gh[pousr]_|npm_|m0[-_]|ntn_)[A-Za-z0-9._-]{8,}\b/gi, REDACTED)
.replace(/\bAIza[A-Za-z0-9_-]{20,}\b/g, REDACTED)
.replace(/((?:^|[\s;])(?:export\s+)?[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\s*=\s*)(?:(['"])[\s\S]*?\2|[^\s;]+)/gim, `$1${REDACTED}`)
.replace(/((?:api[_-]?key|access[_-]?token|cookie|credential|jwt|password|secret|token)\s*[=:]\s*)(?:(['"])[\s\S]*?\2|[^\s,;]+)/gi, `$1${REDACTED}`);
if (home && home !== '/') {
text = text.replace(new RegExp(escapeRegExp(home), 'g'), '~');
}
return text;
}
export function redactOutbound(value, options = {}) {
const seen = new WeakSet();
const walk = (item, key = '') => {
if (item === null || item === undefined || typeof item === 'boolean' || typeof item === 'number') return item;
if (typeof item === 'bigint') return item.toString();
if (typeof item === 'string') return isSensitiveKey(key) ? REDACTED : redactText(item, options);
if (typeof item !== 'object') return redactText(String(item), options);
if (seen.has(item)) return '[Circular]';
seen.add(item);
if (Array.isArray(item)) return item.map(entry => walk(entry));
const result = {};
for (const [childKey, childValue] of Object.entries(item)) {
result[childKey] = isSensitiveKey(childKey) ? REDACTED : walk(childValue, childKey);
}
return result;
};
return walk(value);
}
export function projectLocalIssuesForUpload(issues) {
if (!Array.isArray(issues)) return [];
return issues.slice(0, 100).map(issue => {
const projected = {
severity: issue?.severity,
kind: issue?.kind,
text: issue?.text,
};
if (typeof issue?.knownIssueId === 'string' && ISSUE_ID_PATTERN.test(issue.knownIssueId)) {
projected.knownIssueId = issue.knownIssueId;
}
if (typeof issue?.nativeCheckId === 'string' && ISSUE_ID_PATTERN.test(issue.nativeCheckId)) {
projected.nativeCheckId = issue.nativeCheckId;
}
return projected;
});
}
export function validateFixId(value) {
return typeof value === 'string' && FIX_ID_PATTERN.test(value) ? value : null;
}
export function safeJsonForHtml(value) {
return JSON.stringify(value)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
}
import { opendir } from 'node:fs/promises';
import { join } from 'node:path';
/** Count Markdown files without invoking a shell or following symlinks. */
export async function countMarkdownFiles(root, { maxEntries = 100_000 } = {}) {
if (typeof root !== 'string' || root.length === 0) return 0;
const pending = [root];
let count = 0;
let visited = 0;
while (pending.length > 0 && visited < maxEntries) {
const directory = pending.pop();
let handle;
try {
handle = await opendir(directory);
for await (const entry of handle) {
visited += 1;
if (visited > maxEntries) break;
const path = join(directory, entry.name);
if (entry.isDirectory()) pending.push(path);
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) count += 1;
}
} catch {
// Missing or unreadable paths simply contribute no files.
}
}
return count;
}
+3
-3
{
"name": "clawfix",
"version": "0.8.0",
"version": "0.9.1",
"description": "AI-powered diagnostic and repair for OpenClaw installations",
"bin": {
"clawfix": "./bin/clawfix.js"
"clawfix": "bin/clawfix.js"
},

@@ -21,3 +21,3 @@ "type": "module",

"type": "git",
"url": "https://github.com/arcabotai/clawfix"
"url": "git+https://github.com/arcabotai/clawfix.git"
},

@@ -24,0 +24,0 @@ "bugs": {

@@ -20,9 +20,10 @@ # 🦞 ClawFix

3. **Analyzes** novel problems with AI (optional, with your consent)
4. **Generates** a fix script you can review and run
4. **Builds** a fix script from reviewed deterministic repair snippets; AI never contributes shell
## Privacy
- All secrets, tokens, and API keys are **automatically redacted** before leaving your machine
- ClawFix recursively redacts recognized secrets, tokens, API keys, credentials, and home paths before upload
- Diagnostic data is only sent with your **explicit consent**
- No telemetry, no tracking, no account required
- Error logs are unstructured and may contain identifiers the redactor cannot recognize; inspect locally first
- No account is required; the service uses IP addresses transiently for abuse throttling
- [Source code is open](https://github.com/arcabotai/clawfix) — verify it yourself

@@ -33,4 +34,8 @@

```bash
npx clawfix --yes # Skip confirmation, auto-send diagnostic
npx clawfix -y # Same as above
npx clawfix --dry-run # Local scan, display payload, send nothing
npx clawfix --no-send # Local-only alias
npx clawfix --json # Machine-readable local scan
npx clawfix --show-data # Show payload, then ask before upload
npx clawfix --server URL # Use a custom http(s) API server
npx clawfix --yes # Explicitly skip confirmation and upload
```

@@ -43,2 +48,3 @@

| `CLAWFIX_API` | API endpoint (default: `https://clawfix.dev`) |
| `CLAWFIX_API_TOKEN` | Optional bearer token for a protected ClawFix server |
| `CLAWFIX_AUTO` | Set to `1` to auto-send without prompt |

@@ -45,0 +51,0 @@

Sorry, the diff of this file is too big to display