my 2nd response to TLDR - Prompts don't scale. MCPs don't scale. Hooks do.
You're right - those 3 examples are "lintable". I picked them because they're easy to explain.
Context: I'm building a macOS app (yemreak-macos) - voice dictation with clipboard context for AI conversations. This is my first Swift project, so I'm learning the patterns as I go. Every time I discover a rule that AI keeps violating, I write a hook for it. These examples come from that process.
Think of hooks as a personalized linter - project-specific rules that only make sense in YOUR codebase.
eslint = universal rules (syntax, types) hooks = YOUR rules (architecture, naming conventions, layer boundaries)
eslint can't know "this module shouldn't import that module based on my folder structure"
Concrete Examples
1. Layer Enforcement (Transport can't call Capability)
Source Code:
const CAPABILITY_PATTERN = /\w+Capability\.shared\./
export const noCapabilityInTransportRule: FileRule = {
name: 'noCapabilityInTransport',
active: true,
check(params: FileRuleCheckParams): RuleCheckResult {
// Only check Transport files
const isTransport = params.filePath.includes('/Transport/')
|| params.filePath.endsWith('HTTP.swift')
if (!isTransport) return { matched: false }
const match = params.content.match(CAPABILITY_PATTERN)
if (match) {
return {
matched: true,
violation: `[swift] noCapabilityInTransport violation
File: ${params.filePath}
Found: ${match[0]}
CONSTRAINT: Transport -> Intent (not Capability)
CORRECT:
await IntentExecutor.shared.trigger(.context(.action), source: .http)`
}
}
return { matched: false }
}
}What AI sees when blocked:
[swift] noCapabilityInTransport violation
File: MenuBar.swift
Found: AudioCapability.shared.start()
CONSTRAINT: Transport -> Intent (not Capability)
CORRECT:
await IntentExecutor.shared.trigger(.context(.action), source: .http)2. Architecture Injection on First Read
Source Code:
export const architectureRule: GeneralRule = {
key: 'architecture',
toolMatcher: '^(Read|Write|Edit)$',
async decision(params: RuleContext): Promise<RuleResult> {
const archFiles = await findArchitectureFiles({
cwd: params.cwd,
filePath: filePathResult.path,
scopeType // Read or Write
})
if (archFiles.length === 0) return { type: 'pass' }
// Check if already reminded this session
const reminded = await loadReminded({ cwd: params.cwd, sessionId: params.sessionId })
const newFiles = archFiles.filter(f => !reminded.includes(f.path))
if (newFiles.length === 0) return { type: 'pass' }
// Mark as reminded, then block with architecture content
for (const f of newFiles) {
await markReminded({ cwd: params.cwd, sessionId: params.sessionId, archPath: f.path })
}
return {
type: 'violation',
decision: 'deny',
reason: formatArchitectureAction(newFiles) // <be-aware>...</be-aware>
}
}
}What AI sees when blocked:
<be-aware from="macos/Sources/Contexts/ARCHITECTURE.md">
Transport → Intent → Capability → (return) → Intent → Feedback
Transport: Keyboard entry, menu clicks, HTTP handlers
Intent: Single entry point, orchestration
Capability: External side effects (audio, API, file)
</be-aware>
<retry>Retry action with that awareness</retry>3. Locality Enforcement (MVI Pattern)
Source Code:
const LOCALITY: Record<LocalityType, LocalityConfig> = {
hotkeyLocality: {
patterns: [/\.keyboardShortcut\s*\(/, /addLocalMonitorForEvents/],
allowedSuffix: 'Hotkeys.swift',
allowedExceptions: ['HotkeyCapability.swift'],
message: 'Hotkey code must be in *Hotkeys.swift'
},
panelEscape: {
patterns: [/\.onKeyPress\s*\(\s*\.escape/, /\.keyboardShortcut\s*\(\s*\.escape/],
allowedSuffix: '__NONE__',
allowedExceptions: ['BasePanelController.swift'],
message: 'Panel cannot handle Escape - use Tier 2'
}
}
// In check():
for (const [localityType, config] of Object.entries(LOCALITY)) {
if (fileName.endsWith(config.allowedSuffix)) continue
if (config.allowedExceptions.includes(fileName)) continue
for (const pattern of config.patterns) {
if (pattern.test(line)) {
return { matched: true, violation: generateLocalityViolation(...) }
}
}
}What AI sees when blocked:
[swift] OnMviViolation (panelEscape)
File: DictationPanel.swift
CONSTRAINT: Panel cannot handle Escape - use Tier 2
Found:
Line 23: .keyboardShortcut(.escape)
CORRECT:
Move to BasePanelController (Tier 2 handles Escape)Summary
Layer enforcement
Transport can't call Capability
No
Architecture injection
Show ARCHITECTURE.md on first read
No
Locality
Hotkey code only in *Hotkeys.swift
No
Lint-like
no-magic-number
Yes (examples only)
Last updated
Was this helpful?