clack-prompts — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited clack-prompts (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
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.
Beautiful, opinionated terminal prompts with a connected vertical-bar UI. Used in packages/cli/ for all interactive user flows.
Install: bun add @clack/prompts
Import: import { intro, outro, cancel, isCancel, text, password, confirm, select, multiselect, spinner, log, note, group, tasks } from "@clack/prompts"
Every prompt returns Promise<Value | symbol>. When the user presses Ctrl+C, the return value is a symbol. Always check with isCancel():
import { text, isCancel, cancel } from "@clack/prompts"
const name = await text({ message: "Project name?" })
if (isCancel(name)) {
cancel("Operation cancelled.")
process.exit(0)
}
// After this guard, `name` is narrowed to `string`import { intro, outro, cancel } from "@clack/prompts"
intro("skilltap") // Header bar at start
// ... prompts ...
outro("Done!") // Footer bar at end
// On cancellation:
cancel("Operation cancelled.")
process.exit(0)const name = await text({
message: "What is your project name?",
placeholder: "my-project", // Dimmed hint when empty
initialValue: "", // Pre-filled value
defaultValue: "my-app", // Used if user submits empty
validate(value) {
if (!value) return "Required!"
if (value.length > 50) return "Too long!"
// Return undefined/void = valid
},
})const secret = await password({
message: "Enter your API key:",
mask: "*", // Mask character (default "*")
validate(value) {
if (value.length < 8) return "Must be at least 8 characters"
},
})const shouldInstall = await confirm({
message: "Install anyway?",
initialValue: false, // Default selection
active: "Yes", // Label for "yes" (default "Yes")
inactive: "No", // Label for "no" (default "No")
})
// Returns boolean | symbolconst scope = await select({
message: "Install to:",
options: [
{ value: "global", label: "Global (~/.agents/skills/)" },
{ value: "project", label: "Project (.agents/skills/)", hint: "recommended" },
],
initialValue: "global",
maxItems: 5, // Max visible before scrolling
})
// Returns the selected value's type | symbolOption shape: { value: T, label: string, hint?: string, disabled?: boolean }
const agents = await multiselect({
message: "Auto-symlink to which agents?",
options: [
{ value: "claude-code", label: "Claude Code" },
{ value: "cursor", label: "Cursor" },
{ value: "codex", label: "Codex" },
{ value: "gemini", label: "Gemini" },
{ value: "windsurf", label: "Windsurf" },
],
initialValues: ["claude-code"], // Pre-selected
required: false, // Allow submitting with none selected (default true)
cursorAt: "claude-code", // Initial cursor position
maxItems: 5,
})
// Returns T[] | symbolconst packages = await groupMultiselect({
message: "Select packages:",
options: {
"Frontend": [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
],
"Backend": [
{ value: "express", label: "Express" },
{ value: "fastify", label: "Fastify" },
],
},
required: true,
})
// Selecting a group header toggles all items in the groupconst action = await selectKey({
message: "What to do?",
options: [
{ value: "install", label: "Install", key: "i" },
{ value: "skip", label: "Skip", key: "s" },
],
})
// User presses "i" or "s" to select immediatelyconst s = spinner()
s.start("Installing dependencies...")
// Update message while spinning:
s.message("Almost done...")
// Stop with final message:
s.stop("Dependencies installed.")
// Stop with error (code 1):
s.stop("Installation failed.", 1)All log functions maintain the vertical-bar UI flow:
import { log } from "@clack/prompts"
log.message("A plain message") // Neutral
log.info("Informational") // Blue
log.success("Operation done!") // Green
log.warn("Be careful") // Yellow
log.error("Something broke") // Red
log.step("Step 1: Initialize") // Cyanimport { note } from "@clack/prompts"
note(
"Project: my-app\nFramework: next\nFeatures: typescript, eslint",
"Project Summary" // Optional title
)Renders as a bordered box with the title. Supports \n for multi-line.
Runs prompts in sequence, collects all results into a typed object. Handles cancellation globally.
import { group, text, select, confirm } from "@clack/prompts"
const result = await group(
{
name: () => text({
message: "Project name?",
validate(v) { if (!v) return "Required" },
}),
scope: () => select({
message: "Install scope?",
options: [
{ value: "global", label: "Global" },
{ value: "project", label: "Project" },
],
}),
// Access previous results:
confirm: ({ results }) => confirm({
message: `Install "${results.name}" to ${results.scope}?`,
}),
},
{
onCancel({ results }) {
cancel("Operation cancelled.")
process.exit(0)
},
}
)
// result.name -> string
// result.scope -> "global" | "project"
// result.confirm -> booleanEach prompt function receives { results: Partial<T> } with all prior answers. If any prompt is cancelled and no onCancel is provided, the group throws.
import { tasks } from "@clack/prompts"
await tasks([
{
title: "Cloning repository",
task: async (message) => {
await cloneRepo(url)
message("Scanning for skills...") // Update spinner text
await scanSkills(dir)
return "Repository cloned" // Completion message
},
},
{
title: "Running security scan",
enabled: !skipScan, // Conditionally skip
task: async (message) => {
const warnings = await scanStatic(dir)
return warnings.length
? `${warnings.length} warnings found`
: "No warnings"
},
},
])Task shape: { title: string, task: (message: (msg: string) => void) => Promise<string | void>, enabled?: boolean }
Clack renders a connected vertical-bar UI:
┌ skilltap
│
◆ Install to:
│ ● Global (~/.agents/skills/)
│ ○ Project (.agents/skills/)
│
◇ Auto-symlink to which agents?
│ ◼ Claude Code
│ ◻ Cursor
│
▪───────────────────────╮
│ Skill: commit-helper │
│ Scope: global │
├────────────────────────╯
│
◒ Installing...
│
└ Done!Use picocolors (or similar) for colored text within prompts:
import color from "picocolors"
await text({
message: `What is your ${color.bold("project")} name?`,
})
log.info(color.green("All checks passed!"))The project wraps clack prompts in packages/cli/src/ui/prompts.ts for consistent behavior:
import { select, confirm, isCancel, cancel } from "@clack/prompts"
export async function promptScope(): Promise<"global" | "project"> {
const scope = await select({
message: "Install to:",
options: [
{ value: "global" as const, label: "Global (~/.agents/skills/)" },
{ value: "project" as const, label: "Project (.agents/skills/)" },
],
})
if (isCancel(scope)) {
cancel("Operation cancelled.")
process.exit(2)
}
return scope
}
export async function promptInstall(warnings: boolean): Promise<boolean> {
const result = await confirm({
message: warnings ? "Install anyway?" : "Install?",
initialValue: !warnings, // Default to "no" when there are warnings
active: warnings ? "Yes, install" : "Yes",
inactive: "No",
})
if (isCancel(result)) {
cancel("Operation cancelled.")
process.exit(2)
}
return result
}When agent mode is active, skip all interactive prompts and use plain text output instead. The packages/cli/src/ui/agent-out.ts module handles this:
// Agent mode: no colors, no spinners, no prompts
// Success: "OK: Installed commit-helper → ~/.agents/skills/commit-helper/ (v1.2.0)"
// Error: "ERROR: Repository not found: https://example.com/bad-url.git"
// Security: "SECURITY ISSUE FOUND — INSTALLATION BLOCKED\n..."Check config['agent-mode'].enabled early and branch to agent output functions instead of interactive prompts.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.