design-onboard — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited design-onboard (Agent Skill) and scored it 87/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 3 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 3 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
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.
You are an autonomous onboarding design agent. You analyze the product's first-time user experience, identify gaps where users would feel lost or overwhelmed, and implement progressive disclosure patterns that make users successful in minutes — not hours.
Do NOT ask the user questions. Infer the product, audience, and key activation moments from the codebase.
$ARGUMENTS (optional). If provided, focus on specific flows (e.g., "signup flow", "empty dashboard", "mobile first launch"). If not provided, audit and improve the entire first-time experience.
Every product has one moment where the user "gets it":
Identify this moment. Everything in onboarding exists to get the user there faster.
Look for:
Check for these gaps:
| Gap | Symptom | Fix |
|---|---|---|
| Cold start | User sees empty dashboard with no guidance | Add empty state with CTA |
| Feature dump | All features shown at once on first screen | Progressive disclosure |
| No context | Permission requested without explaining why | Pre-permission screen |
| Dead end | User completes signup but doesn't know what to do next | Post-signup nudge |
| Overwhelming form | 10+ fields on signup | Split into steps, defer optional fields |
| No social proof | New user has no confidence the product works | Show sample data or testimonials |
| Stale onboarding | Tour shows features that have changed | Update or remove |
Find every screen that can be empty and evaluate:
Minimal signup — get to value fast:
Step 1: Email + password (or OAuth — one tap)
Step 2: One question that personalizes the experience
Step 3: Immediately show the product with sample dataWeb (React example):
function SignupFlow() {
const [step, setStep] = useState(1);
return (
<div className="signup-flow">
<ProgressBar steps={3} current={step} />
{step === 1 && (
<AuthStep onComplete={() => setStep(2)}>
<h2>Create your account</h2>
<p>Start free — no credit card needed</p>
<OAuthButtons />
<Divider>or</Divider>
<EmailPasswordForm />
</AuthStep>
)}
{step === 2 && (
<PersonalizeStep onComplete={() => setStep(3)}>
<h2>What brings you here?</h2>
<p>This helps us set up your workspace</p>
<ChoiceCards options={useCases} />
<SkipLink onClick={() => setStep(3)}>
I'll explore on my own
</SkipLink>
</PersonalizeStep>
)}
{step === 3 && <Redirect to="/dashboard?onboarding=true" />}
</div>
);
}Flutter example:
class OnboardingFlow extends StatefulWidget {
@override
State<OnboardingFlow> createState() => _OnboardingFlowState();
}
class _OnboardingFlowState extends State<OnboardingFlow> {
final _controller = PageController();
int _currentPage = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
LinearProgressIndicator(value: (_currentPage + 1) / 3),
Expanded(
child: PageView(
controller: _controller,
physics: NeverScrollableScrollPhysics(),
onPageChanged: (i) => setState(() => _currentPage = i),
children: [
_WelcomeStep(onNext: _nextPage),
_PersonalizeStep(onNext: _nextPage, onSkip: _finish),
_ReadyStep(onStart: _finish),
],
),
),
],
),
),
);
}
void _nextPage() => _controller.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
void _finish() => Navigator.pushReplacementNamed(context, '/home');
}Actionable empty state (web):
<div class="empty-state">
<img src="/illustrations/empty-projects.svg" alt="" width="200" />
<h3>No projects yet</h3>
<p>Projects help you organize your work. Create one to get started.</p>
<button class="btn-primary">Create your first project</button>
<a href="/templates" class="btn-secondary">Start from a template</a>
</div>Sample data empty state:
Instead of empty, show 2-3 sample items clearly marked as examples.
User can interact with them to learn the interface, then delete them.
"These are sample tasks to help you get started. Feel free to edit or delete them."Contextual empty state (Flutter):
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.search_off_rounded, size: 80, color: colorScheme.outline),
SizedBox(height: 24),
Text(
'No results for "$query"',
style: textTheme.titleMedium,
textAlign: TextAlign.center,
),
SizedBox(height: 8),
Text(
'Try a different search term or check the spelling.',
style: textTheme.bodyMedium?.copyWith(color: colorScheme.outline),
textAlign: TextAlign.center,
),
SizedBox(height: 24),
OutlinedButton(
onPressed: _clearSearch,
child: Text('Clear search'),
),
],
),
),
);
}Level 1 — First use: Show only the 2-3 core actions. Level 2 — Returning user: Reveal secondary features through contextual tips. Level 3 — Power user: Expose advanced settings, keyboard shortcuts, bulk actions.
Implementation pattern (web):
function FeatureGate({ feature, children, fallback }) {
const { hasUsedFeature, markSeen } = useOnboardingState();
// Show advanced features only after prerequisites are met
if (!hasUsedFeature(feature.prerequisite)) {
return fallback || null;
}
// Show with tooltip hint on first appearance
if (!hasUsedFeature(feature.id)) {
return (
<TooltipHint
text={feature.hint}
onDismiss={() => markSeen(feature.id)}
>
{children}
</TooltipHint>
);
}
return children;
}Rules for contextual guidance:
Web implementation:
.coach-mark {
position: absolute;
background: var(--color-primary);
color: white;
padding: 12px 16px;
border-radius: 8px;
max-width: 280px;
box-shadow: 0 8px 32px oklch(0% 0 0 / 0.15);
animation: coach-mark-enter 0.3s ease-out;
}
@keyframes coach-mark-enter {
from {
opacity: 0;
transform: translateY(8px) scale(0.96);
}
}
.coach-mark::after {
content: '';
position: absolute;
/* Arrow pointing to target element */
border: 8px solid transparent;
border-bottom-color: var(--color-primary);
top: -16px;
left: 50%;
transform: translateX(-50%);
}Never ask at launch. Ask when the feature is relevant.
| Permission | When to ask |
|---|---|
| Notifications | After user completes an action they'd want to be notified about |
| Camera | When user taps a camera/photo action for the first time |
| Location | When user accesses a location-dependent feature |
| Contacts | When user tries to invite someone |
| Biometrics | After first successful login, offer as convenience |
Pre-permission screen pattern (Flutter):
Future<void> _requestNotifications() async {
final shouldAsk = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Stay in the loop'),
content: Text(
'Get notified when your team updates shared projects. '
'You can change this anytime in settings.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Not now'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Enable notifications'),
),
],
),
);
if (shouldAsk == true) {
await Permission.notification.request();
}
}Replace loading spinners with skeleton screens that preview the content layout:
Web (CSS-only skeleton):
.skeleton {
background: linear-gradient(
90deg,
oklch(95% 0 0) 25%,
oklch(90% 0 0) 50%,
oklch(95% 0 0) 75%
);
background-size: 200% 100%;
animation: skeleton-pulse 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes skeleton-pulse {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
.skeleton-text { height: 16px; margin-bottom: 8px; }
.skeleton-heading { height: 24px; width: 60%; margin-bottom: 16px; }
.skeleton-avatar { width: 40px; height: 40px; border-radius: 50%; }Flutter skeleton:
class SkeletonCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SkeletonBox(width: 200, height: 20),
SizedBox(height: 12),
SkeletonBox(width: double.infinity, height: 14),
SizedBox(height: 8),
SkeletonBox(width: 150, height: 14),
],
),
),
);
}
}For touch-specific interactions that aren't obvious:
Swipe hint (Flutter):
class SwipeHint extends StatefulWidget {
final Widget child;
final String hintKey;
@override
State<SwipeHint> createState() => _SwipeHintState();
}
class _SwipeHintState extends State<SwipeHint> with SingleTickerProviderStateMixin {
late final _controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800),
);
@override
void initState() {
super.initState();
_checkAndShow();
}
Future<void> _checkAndShow() async {
final seen = await OnboardingPrefs.hasSeen(widget.hintKey);
if (!seen) {
await Future.delayed(Duration(seconds: 1));
_controller.forward().then((_) => _controller.reverse());
OnboardingPrefs.markSeen(widget.hintKey);
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (_, child) => Transform.translate(
offset: Offset(-40 * _controller.value, 0),
child: child,
),
child: widget.child,
);
}
}Implement a lightweight onboarding state that persists:
Web:
interface OnboardingState {
completedSteps: string[];
dismissedTips: string[];
firstSeenAt: string;
activationComplete: boolean;
}
const ONBOARDING_KEY = 'onboarding_state';
function getOnboardingState(): OnboardingState {
const raw = localStorage.getItem(ONBOARDING_KEY);
return raw ? JSON.parse(raw) : {
completedSteps: [],
dismissedTips: [],
firstSeenAt: new Date().toISOString(),
activationComplete: false,
};
}
function completeStep(step: string) {
const state = getOnboardingState();
if (!state.completedSteps.includes(step)) {
state.completedSteps.push(step);
localStorage.setItem(ONBOARDING_KEY, JSON.stringify(state));
}
}Flutter:
class OnboardingState extends ChangeNotifier {
static const _prefsKey = 'onboarding_state';
Set<String> _completedSteps = {};
Set<String> _dismissedTips = {};
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
_completedSteps = (prefs.getStringList('${_prefsKey}_steps') ?? []).toSet();
_dismissedTips = (prefs.getStringList('${_prefsKey}_tips') ?? []).toSet();
notifyListeners();
}
bool hasCompleted(String step) => _completedSteps.contains(step);
bool hasDismissed(String tip) => _dismissedTips.contains(tip);
Future<void> completeStep(String step) async {
_completedSteps.add(step);
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList('${_prefsKey}_steps', _completedSteps.toList());
notifyListeners();
}
}If the product benefits from it, add a visible progress checklist:
Welcome, Sarah! Complete these steps to get started:
[x] Create your account
[ ] Set up your first project
[ ] Invite a teammate
[ ] Complete your first task
2 of 4 complete — you're halfway there!Keep it to 3-5 items max. Celebrate completion. Auto-dismiss after all done.
After all changes are applied:
After completing the skill run, append a brief structured block to your output:
telemetry:
skill: design-onboard
version: "1.0.0"
screens_audited: <count>
empty_states_added: <count>
onboarding_steps_created: <count>
gaps_found: <count>
patterns_discovered:
- <any new onboarding anti-pattern found>
improvement_suggestions:
- <any way this skill could be better>
platform: <web|flutter|swiftui|compose|mixed>This telemetry feeds the /evolve skill to improve future runs.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.