kastell-ops — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited kastell-ops (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 2 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Kastell is a CLI toolkit for provisioning, securing, and managing self-hosted servers. TypeScript, ESM, strict mode. 31 CLI commands, 13 MCP tools, 4 cloud providers (hetzner, digitalocean, vultr, linode), 2 platform adapters (coolify, dokploy).
Version: !node -e "import('fs').then(f=>console.log(JSON.parse(f.readFileSync('package.json','utf8')).version)).catch(()=>console.log('unknown'))" Registered servers: !kastell list 2>/dev/null || echo "No servers registered"
src/
commands/ # 31 thin CLI wrappers (parse args + delegate only)
core/ # Business logic (ALL computation here)
providers/ # Cloud API: hetzner, digitalocean, vultr, linode
adapters/ # Platform abstraction: coolify, dokploy
interface.ts # PlatformAdapter contract
factory.ts # getAdapter(), detectPlatform(), resolvePlatform()
mcp/
server.ts # 13 tool registrations
tools/ # Handler files (Zod schema + handler per tool)
utils/ # ssh, config, cloudInit, modeGuard, migration
types/ # ServerMode, ServerRecord, Platform
constants.ts # PROVIDER_REGISTRY (single source of truth)
index.ts # CLI entry point| Layer | Path | Responsibility | Rule |
|---|---|---|---|
| Commands | src/commands/ | Parse CLI args, call core, display output | ZERO business logic |
| Core | src/core/ | All business logic, orchestration | No UI/chalk/ora imports |
| Providers | src/providers/ | Cloud API calls per provider | Implements cloud CRUD |
| Adapters | src/adapters/ | Platform-specific ops (Coolify/Dokploy) | Via PlatformAdapter interface |
| MCP | src/mcp/ | MCP server + tool handlers | Delegates to core |
| Utils | src/utils/ | SSH, config, modeGuard, errorMapper | Shared infrastructure |
Access adapters via getAdapter(platform) from src/adapters/factory.ts. Never import CoolifyAdapter or DokployAdapter directly in commands.
interface PlatformAdapter {
readonly name: string; // "coolify" | "dokploy"
readonly port: number; // 8000 (Coolify) | 3000 (Dokploy)
readonly defaultLogService: string; // matches platform name
readonly platformPorts: readonly number[]; // ports protected from firewall removal
getCloudInit(serverName: string): string;
healthCheck(ip: string, domain?: string): Promise<HealthResult>;
createBackup(ip: string, serverName: string, provider: string): Promise<PlatformBackupResult>;
getStatus(ip: string): Promise<PlatformStatusResult>;
update(ip: string): Promise<UpdateResult>;
restoreBackup?(ip, backupPath, manifest): Promise<PlatformRestoreResult>; // optional
}Factory exports: getAdapter(platform), detectPlatform(ip), resolvePlatform(server)
| Provider | Env Key | Display Name |
|---|---|---|
| hetzner | HETZNER_TOKEN | Hetzner Cloud |
| digitalocean | DIGITALOCEAN_TOKEN | DigitalOcean |
| vultr | VULTR_TOKEN | Vultr |
| linode | LINODE_TOKEN | Linode (Akamai) |
PROVIDER_REGISTRY in src/constants.ts is the single source of truth for providers.
src/commands/<name>.ts — thin wrapper (parse + delegate, no logic)src/core/<name>.ts — all business logic heresrc/index.ts — register with programsrc/__tests__/ — test core, not commandsrc/core/audit/<category>/ — add to existing categorysrc/core/audit/catalog.tskastell auditsrc/providers/<name>.ts — implements base.ts contractsrc/constants.ts — add to PROVIDER_REGISTRYsrc/mcp/tools/server<Name>.ts — Zod schema + handlersrc/mcp/server.ts — import + registerTool()readOnlyHint / destructiveHint / idempotentHint"type": "module") — import, not requireKASTELL_SAFE_MODE + isSafeMode() = destructive operation guardassertValidIp() before every SSH operationsanitizedEnv for subprocess callssanitizeResponseData() whitelist approach for API error responseskastell/ directory (auto-migrated from legacy name)PROVIDER_REGISTRY = single source of truth for providerswithProviderErrorHandling HOF for consistent provider error handlingdescribe.each with jest.resetAllMocks() (not clearAllMocks())Run without LLM — deterministic analysis scripts:
# Parse audit JSON into 5 security domain summaries
kastell audit --server myserver --json > /tmp/audit.json
scripts/parse_audit.sh /tmp/audit.json
# Generate fleet-wide server score table
kastell fleet --json > /tmp/fleet.json
scripts/fleet_report.sh /tmp/fleet.json
# Compare audit check count vs test coverage
scripts/check_coverage.sh~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.