npx skills add ...
npx skills add hegargarcia/skills --skill personal-code-style
Hegar's personal, cross-repo coding-style preferences — general taste, not any single project's product decisions. Apply silently as a guardrail whenever you write, edit, refactor, review, or generate code, tests, schemas, migrations, scripts, or code-describing docs. Defer to current-turn instructions and repo conventions first; use this to fill gaps. Update it when Hegar gives durable, repo-agnostic feedback about code shape, abstractions, naming, types, validation, errors, libraries, tooling, or diffs.
npx skills add hegargarcia/skills --skill personal-code-style
Hegar's general coding taste, distilled from day-to-day feedback across many repos. This captures how Hegar likes code to be shaped — not the product or architecture decisions of any one project. The same file is shared by every agent Hegar works with (Claude and Codex), so edits here propagate everywhere.
Precedence, highest first:
If a rule here conflicts with current feedback or a clear repo convention, follow the feedback or convention. If that feedback is durable and repo-agnostic, also update this skill (see Updating this skill).
and/or.fetch, get, resolve) over side-effect verbs (refresh).input* argument names when the value is already a function input. Use the domain noun directly unless Input is part of the domain term.build* names for query/predicate helpers; say whether the helper resolves a column predicate or adapts an EXISTS predicate.agent.ts, types.ts, client.ts, service.ts) and split out a narrower file only once one earns it. Don't repeat the directory/domain name in the filename.SomeHelperContext alias unless it's reused or clarifies a shared contract.{ actorId, db }, { createdBy, tx }). Use an object for the primary value when it has multiple fields; pass the scalar directly when it's a single identifier (e.g. userProfileId). Don't pass long positional dependency lists. (See Hard cases.){ db }, { actorId, db }) in a shared domain-context type instead of redefining local aliases.if blocks over ternaries. Reserve ternaries for compact local value selection — never for awaited work or as part of a larger statement.map + filter over flatMap unless one input can produce multiple outputs.!!value when coalescing truthiness to a boolean instead of comparing to === true.Result value ({ ok: true; data } | { ok: false; error }) over throwing for expected failures the system should inspect, log, or route. Reserve throwing for genuinely exceptional paths. (See Hard cases.)Error — never model them as standalone plain object types.as const for small stable object shapes over noisy return annotations.typeof revalidation, and don't revalidate data you already own — type trusted internal responses and trust the contract. Reserve runtime validation for untyped/untrusted boundaries..passthrough(). Use z.looseObject({ ... }) for permissive objects (e.g. third-party payloads whose unknown fields should survive parsing), or an explicit .catchall(...) only when unknown values must be validated.preprocess, transform, pipe, or codecs. Do not parse a source shape and then run a separate secondary normalization pass, and do not share context-specific boundary schemas just because the fields happen to overlap. (See Hard cases.).transform() runs after the inner schema validates. When normalizing raw source input so it can validate, use z.preprocess or z.coerce.* before validation, then use .pipe when the normalized output needs another validation pass.z.coerce.* schemas and narrow z.preprocess helpers over codecs, stacked pipes, or transform chains — unless you genuinely need bidirectional codec behavior. The exception: a JSON-string field with a known output schema and a string input contract, where a typed Zod codec is right (invalid JSON should surface as a validation issue).z.coerce.number(), z.url(), and .prefault() over handwritten parsing, URL normalization, or missing-value defaults when they express the source contract..prefault(defaultValue) for missing-input defaults that should still run through validation. Do not convert null to a default unless the source contract treats null as missing.T extends z4.$ZodType); don't erase it behind z.unknown(). (See Hard cases.)safeParse once and branch on the result — don't parse in a try/catch and then re-parse for diagnostics or cursor extraction. If several getters repeat the same validation loop, extract a generic helper that owns the safeParse branch and returns the typed parsed row.db/paths module directly rather than threading a context object through every call. Add DI only when there's a real seam to vary.fetch wrappers and response casts.query.ts for query execution, schema.ts for row-contract parsing) when a module owns both.Bun.file, Bun.write). Use node: imports where Bun doesn't expose an operation (e.g. some directory/path work). Don't write custom I/O helpers when Bun or the standard runtime already covers the operation.simple-git, use the typed client returned by simpleGit() directly; don't hide it behind homemade command wrappers. Use .raw only when there's no typed method for the operation.cast or aliasedColumn helper) for reusable operations instead of inline casts, raw sql aliasing, or string fragments. Keep operator semantics in shared query helpers — feature code passes columns, predicates, or relation wrappers rather than reimplementing each filter operator locally. Normalize search text at the shared helper boundary, and type custom search builders to return a concrete SQL predicate.coalesce(table.updatedAt, table.createdAt) in query selects over post-query shape repair helpers.enum config on text columns for TypeScript safety, even where the database has no enum type (e.g. SQLite)..$defaultFn for runtime-generated defaults like IDs and timestamps — especially on SQLite, which has no timestamp column type. Prefer DB-side now() for audit timestamps inside conflict-update SQL, so the recorded time is tied to the transaction.drizzle-kit, etc.); don't hand-create migration/backfill files, hand-roll migration plumbing, or rewrite generated migrations into custom control flow. Prefer the generated drop/create shape unless the user asks for custom migration SQL.React.cache for shared route fetch logic only when the callers need the same fields at the same level of detail.cell() {}) over property-arrow callbacks (cell: () => {}).git status and the final diff after formatters, codegen, hooks, or generated artifacts run.Worked examples for the rules that are hardest to convey in a sentence.
Reach for an abstraction once it removes real complexity or duplication — not to name a single expression.
Pass the scalar directly when the primary value is a single identifier; use an object once it has multiple fields.
A cast is a last resort; reach for a guard or schema first.
Throw only for genuinely exceptional paths. For failures the system should inspect, log, or route, return them as values.
The schema mirrors the boundary; renaming is a concern of the consumer, not the validator.
Hegar has authorized agents to keep this skill current from his feedback. This is the single shared file for all agents — patching it here updates the guidance everywhere.
When Hegar gives feedback about code — including terse corrections like "don't use classes", "use Drizzle", "don't wrap that in a function", "too much abstraction", or "don't run that every time":
// Avoid: a long positional dependency list — the call site is unreadable.
async function archiveProfile(profileId: string, actorId: string, db: Db, tx: Tx) {}
// Preferred: primary domain value first, operational deps in one context object.
async function archiveProfile(profileId: string, { actorId, db }: AppContext) {}
// When the primary value has several fields, pass it as an object too.
async function createNote(note: { body: string; profileId: string }, { createdBy, tx }: WriteContext) {}// Avoid: assert the shape with a cast — `raw` was never actually checked.
const payload = JSON.parse(raw) as WebhookEvent
handle(payload.type)
// Preferred: validate at the boundary, then the type is earned, not asserted.
const parsed = webhookEventSchema.safeParse(JSON.parse(raw))
if (!parsed.success) return { ok: false, error: parsed.error }
handle(parsed.data.type)type Result<T, E = string> = { ok: true; data: T } | { ok: false; error: E }
async function loadConfig(path: string): Promise<Result<Config>> {
const file = Bun.file(path)
if (!(await file.exists())) return { ok: false, error: "missing" }
return { ok: true, data: parseConfig(await file.text()) }
}
const res = await loadConfig(path)
if (!res.ok) return renderMissingConfig(res.error) // expected, handled inline
use(res.data)// Avoid: a transform that exists only to rename provider fields snake -> camel.
const providerUser = z.object({
first_name: z.string(),
created_at: z.string(),
}).transform((u) => ({ firstName: u.first_name, createdAt: u.created_at }))
// Preferred: parse the source shape as-is; map names where the value is consumed.
const providerUser = z.object({ first_name: z.string(), created_at: z.string() })
const view = { firstName: parsed.first_name, createdAt: parsed.created_at }// Erases the caller's type — `data` arrives as `unknown`.
function defineEndpoint(input: z4.$ZodType, handler: (data: unknown) => void) {}
// Preferred: thread the schema through a generic so the inferred type survives.
function defineEndpoint<T extends z4.$ZodType>(
input: T,
handler: (data: z4.infer<T>) => void,
) {}
// `handler` now receives the caller's exact inferred type, not `unknown`.