Claude Code skill library

r3ckon-style

The rules I would otherwise retype at the top of every session. Six skills, one plugin. Install it as is, or fork it and swap in your own.

install
> /plugin marketplace add localhostd3veloper/r3ckon-style
> /plugin install r3ckon-style@r3ckon-style

Skills

six

Each one loads itself from its description, so a normal request activates the right skill. Force one with /code-style, /style-review, and so on. code-style is the base; frontend and backend assume it and cover only their own side.

code-style
Writing or editing code in any language. The universal rules.
frontend-standards
React components, hooks, App Router pages, Tailwind, shadcn.
backend-standards
Route handlers, server actions, MongoDB, API contracts.
style-review
Reviewing a diff or branch against all of the above.
git-conventions
Writing a commit message, naming a branch, opening a PR.
humanize
Writing or editing any prose a person will read.

The rules

12 shown

Every rule ships with its replacement. A ban on its own just gets worked around.

01

Comments

code-style

Rename or restructure until the code reads. The name holds what the comment would have said, and it cannot go stale.

instead of a comment
// grace period is 15 min after publish
if (Date.now() - post.publishedAt < 900_000) {
  ...
}
a name
const EDIT_GRACE_PERIOD_MS = 15 * 60 * 1000

function isWithinEditGracePeriod(post: Post) {
  return Date.now() - post.publishedAt < EDIT_GRACE_PERIOD_MS
}

if (isWithinEditGracePeriod(post)) {
  ...
}
02

Em dashes

code-style

Not in code, string literals, UI copy, prose, or commit messages. A comma, a colon, parentheses, or a full stop.

instead of
toast.error("Session expired — sign in again")
this
toast.error("Session expired. Sign in again.")
03

Branching

code-style

Three or more branches on the same discriminant becomes a switch. Drop default on a closed union and TypeScript fails the build when someone adds a case and forgets this file, which is the point.

instead of an if/else ladder
if (status === "queued") {
  return "Queued"
} else if (status === "running") {
  return "In progress"
} else if (status === "failed") {
  return "Failed"
} else {
  return "Done"
}
a switch
switch (status) {
  case "queued":
    return "Queued"
  case "running":
    return "In progress"
  case "failed":
    return "Failed"
  case "done":
    return "Done"
}
04

Nesting

code-style

Handle the exits first, then write the happy path flat at the bottom. Past two levels inside a function, extract or invert.

instead of nested conditionals
if (post) {
  if (post.authorId === user.id) {
    if (post.status === "draft") {
      return publish(post)
    } else {
      throw new ConflictError()
    }
  } else {
    throw new ForbiddenError()
  }
} else {
  throw new NotFoundError()
}
guard clauses
if (!post) throw new NotFoundError()
if (post.authorId !== user.id) throw new ForbiddenError()
if (post.status !== "draft") throw new ConflictError()

return publish(post)
05

State that mirrors props

frontend-standards

Never call setState inside useEffect. If a value can be computed from props or existing state, compute it during render.

instead of an effect
const [fullName, setFullName] = useState("")

useEffect(() => {
  setFullName(`${first} ${last}`)
}, [first, last])
derive it
const fullName = `${first} ${last}`
06

Resetting state

frontend-standards

To clear state when an identity changes, remount the subtree with a key. React throws the old state away for you.

instead of clearing by hand
useEffect(() => {
  setDraft("")
}, [conversationId])
remount
<MessageComposer key={conversationId} />
07

Class composition

frontend-standards

Everything goes through cn(), because tailwind-merge cannot resolve a conflict it cannot see inside a template literal.

instead of
className={`p-2 ${isActive ? "bg-accent" : ""} ${className}`}
this
className={cn("p-2", isActive && "bg-accent", className)}
08

Input

backend-standards

Parse, do not validate. The schema's output type is what the rest of the function sees, so an unparsed value is never in scope. Bodies, query strings, params, webhooks, env vars, third-party responses.

instead of trusting it
export async function POST(request: Request) {
  const body = await request.json()
  await createInvite(body.email, body.role)
}
parse at the boundary
const CreateInvite = z.object({
  email: z.string().email("Enter a valid email address"),
  role: z.enum(["admin", "member"]),
})

export async function POST(request: Request) {
  const parsed = CreateInvite.safeParse(await request.json())
  if (!parsed.success) return badRequest(parsed.error)

  await createInvite(parsed.data)
}
09

Ownership

backend-standards

Scope the query by the authenticated principal instead of fetching and then checking. It also avoids telling an attacker that a resource exists but is not theirs.

instead of fetch, then check
const project = await projects.findOne({
  _id: new ObjectId(id),
})

if (project.ownerId !== session.userId) {
  throw new ForbiddenError()
}
scope the filter
const project = await projects.findOne({
  _id: new ObjectId(id),
  ownerId: session.userId,
})

if (!project) throw new NotFoundError()
10

Responses

backend-standards

Database documents grow fields. A handler that spreads one into a response leaks whatever someone adds next year. One mapper per resource, _id to a string id, dates to ISO.

instead of the raw document
return Response.json(user)

return Response.json({
  ...user,
  passwordHash: undefined,
})
an explicit mapper
function toPublicUser(user: UserDocument) {
  return {
    id: user._id.toString(),
    email: user.email,
    role: user.role,
  }
}
11

Commit subjects

git-conventions

Imperative, lowercase, no trailing period, under 72 characters. Optimize for git log --oneline and blame, not for the GitHub commit page. Say what changed, not that something changed.

instead of
feat: Added a new endpoint for revoking sessions.
feat: adds session revocation endpoint
feat: session revocation
this
feat(auth): add session revocation endpoint
12

Branch names

git-conventions

type/ticket-description. Lowercase, hyphenated, three to five words. Ticket ids keep their upstream casing. Without a tracker, drop the segment rather than inventing a number.

instead of
Feature/AddLoginPage
feature/add_login_page
gautam-working-branch
this
feature/PROJ-123-add-login-page
fix/PROJ-88-session-expiry-rounding
hotfix/revoke-leaked-api-keys
and the rest

File length

Under 200 lines on the frontend, under 1000 everywhere else. A refactor trigger, not a lint error. Split it, do not compress it.

Names

Units live in the name and booleans read as assertions: timeoutMs, hasPendingInvite. Never data or utils as a whole name.

Three exceptions

Zod .describe(), //#region splitting queries from mutations in a hook file, and a named workaround for a bug in someone else's code.

Committing

Finish, build, report, then ask. No commit without an explicit yes, and none carrying a Co-Authored-By: Claude trailer.

Make it yours

fork

Copy templates/SKILL.template.md into plugins/r3ckon-style/skills/<name>/SKILL.md. The description is the only part read when deciding whether to load a skill, so name real triggers: file types, libraries, the words you would use in an actual request.

"Best practices for code quality" never fires. "React components, hooks, App Router pages, Tailwind classes" does.

To iterate without reinstalling after every edit, symlink the skill into your user directory. Skills reload on the next session.

ln -s "$PWD/plugins/r3ckon-style/skills/code-style" \
  ~/.claude/skills/code-style