Behavioral guidelines to reduce common LLM coding mistakes, derived from Andrej Karpathy's observations on LLM coding pitfalls.
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
Touch only what you must. Clean up only your own mess.
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
Relay is a native macOS Matrix client built with SwiftUI. The codebase is organized into three layers:
- Relay/ -- App target (SwiftUI views, entry point)
- RelayKit/ -- Framework target (Matrix Rust SDK integration, services, view models)
- Packages/RelayInterface/ -- Local SPM package (shared protocols and model types, zero dependencies)
Views program against RelayInterface protocols, not concrete SDK types.
Only RelayApp.swift imports RelayKit directly.
- Open
Relay.xcodeprojin Xcode 26+. - Build:
Cmd+Bwith the Relay scheme selected. - Run tests:
Cmd+Uor usexcodebuild test. - Requires macOS 26.0 (Tahoe) or later.
- Swift 6 with strict concurrency (
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor). RespectSendableand actor-isolation rules. - Prefer
Task @MainActoroverDispatchQueue.main.asyncfor main queue execution. - Use
@Observableand@Environmentfor state management. - Bridge SDK callbacks to Swift concurrency with
AsyncStream. - Keep commits focused and atomic. Use imperative mood, sentence-case commit messages (e.g. "Add thread support to timeline view").
- Comments should reflect the current state of the code. Documentation should not discuss previous iterations of the code, only the current one.
- Follow standard, idiomatic, Apple-recommended guidelines as much as possible.
- Include a summary of what changed in the commit message.
- When authoring a commit, use either
Assisted-By: <name of code assistant>orGenerated-By: <name of code assistant"in the commit message footer.- Assisted-By: You directed the work and edited meaningfully (default for typical use).
- Generated-By: A substantial portion was generated with minimal human edit (e.g. full file scaffold).
- Never push commits without explicit approval from the user.
- Never import
MatrixRustSDKorRelayKitfrom view code. Views depend only onRelayInterfaceprotocols. - New SDK wrappers go in
RelayKit/. New protocols and shared models go inPackages/RelayInterface/. - Previews must work without loading the Rust binary. Use mock
implementations that conform to
RelayInterfaceprotocols.
Always verify UI changes against the latest Apple Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines/
Relay should look and feel like a first-class macOS app, not a cross-platform or web-based client. When in doubt, reference native Apple apps (Messages, Mail) for interaction patterns, spacing, and typography. Key points:
- Use standard macOS controls and layout conventions.
- Respect system settings (appearance, accent color, accessibility).
- Prefer SF Symbols for iconography.
- Follow platform conventions for navigation, toolbars, and sidebars.
@Observableclasses must be marked@MainActorunless the project has Main Actor default actor isolation. Flag any@Observableclass missing this annotation.- All shared data should use
@Observableclasses with@State(for ownership) and@Bindable/@Environment(for passing). - Strongly prefer not to use
ObservableObject,@Published,@StateObject,@ObservedObject, or@EnvironmentObjectunless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated. - Assume strict Swift concurrency rules are being applied.
- Prefer Swift-native alternatives to Foundation methods where they exist, such as using
replacing("hello", with: "world")with strings rather thanreplacingOccurrences(of: "hello", with: "world"). - Prefer modern Foundation API, for example
URL.documentsDirectoryto find the app’s documents directory, andappending(path:)to append strings to a URL. - Never use C-style number formatting such as
Text(String(format: "%.2f", abs(myNumber))); always useText(abs(change), format: .number.precision(.fractionLength(2)))instead. - Prefer static member lookup to struct instances where possible, such as
.circlerather thanCircle(), and.borderedProminentrather thanBorderedProminentButtonStyle(). - Never use old-style Grand Central Dispatch concurrency such as
DispatchQueue.main.async(). If behavior like this is needed, always use modern Swift concurrency. - Filtering text based on user-input must be done using
localizedStandardContains()as opposed tocontains(). - Avoid force unwraps and force
tryunless it is unrecoverable. - Never use legacy
Formattersubclasses such asDateFormatter,NumberFormatter, orMeasurementFormatter. Always use the modernFormatStyleAPI instead. For example, to format a date, usemyDate.formatted(date: .abbreviated, time: .shortened). To parse a date from a string, useDate(inputString, strategy: .iso8601). For numbers, usemyNumber.formatted(.number)or custom format styles.
- Always use
foregroundStyle()instead offoregroundColor(). - Always use
clipShape(.rect(cornerRadius:))instead ofcornerRadius(). - Always use the
TabAPI instead oftabItem(). - Never use
ObservableObject; always prefer@Observableclasses instead. - Never use the
onChange()modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none. - Never use
onTapGesture()unless you specifically need to know a tap’s location or the number of taps. All other usages should useButton. - Never use
Task.sleep(nanoseconds:); always useTask.sleep(for:)instead. - Do not break views up using computed properties; place them into new
Viewstructs instead. - Do not force specific font sizes; prefer using Dynamic Type instead.
- Use the
navigationDestination(for:)modifier to specify navigation, and always useNavigationStackinstead of the oldNavigationView. - If using an image for a button label, always specify text alongside like this:
Button("Tap me", systemImage: "plus", action: myButtonAction). - Don’t apply the
fontWeight()modifier unless there is good reason. If you want to make some text bold, always usebold()instead offontWeight(.bold). - Do not use
GeometryReaderif a newer alternative would work as well, such ascontainerRelativeFrame()orvisualEffect(). - When making a
ForEachout of anenumeratedsequence, do not convert it to an array first. So, preferForEach(x.enumerated(), id: \.element.id)instead ofForEach(Array(x.enumerated()), id: \.element.id). - When hiding scroll view indicators, use the
.scrollIndicators(.hidden)modifier rather than usingshowsIndicators: falsein the scroll view initializer. - Use the newest ScrollView APIs for item scrolling and positioning (e.g.
ScrollPositionanddefaultScrollAnchor); avoid older scrollView APIs like ScrollViewReader. - Place view logic into view models or similar, so it can be tested.
- Avoid
AnyViewunless it is absolutely required. - Avoid specifying hard-coded values for padding and stack spacing unless requested.