Author: Theodor Vararu
Last updated: 07/04/2026
This is a curated set of recipes I use across multiple projects.
It's not really a SKILL.md, not really a template. It's stuff I've had to ask Claude to "hey can you copy how we do FooThing in ../project."
It's not broken up into multiple files, because it's designed to be copy and pasted as one chunk, whenever I need to align my projects to the same conventions, or when I'm scaffolding a new one.
I use this simple heuristic to decide what to use for a new project:
-
rustif it requires knowing what a pointer is -
bunfor everything else
Some projects use both.
All my projects use mise. The /mise Claude skill in my dotfiles has some more details about the exact setup.
[tools]
"cargo:cargo-llvm-cov" = "latest"
"npm:@biomejs/biome" = "latest"
blender = "latest"
bun = "latest"
godot = "latest"
hk = "latest"
jq = "latest"
pkl = "latest"
rust = "latest"
[settings]
npm.package_manager = "bun"If I need a tool to do something in a project, I pull it in using mise. I
avoid brew or apt as it's more portable this way.
I don't pull in linters/formatters/miscellaneous tools through package.json if
I can avoid it. This reduces dependabot noise.
npm.package_manager = "bun" to prevent accidentally mixing node.
[tasks."rs:test"]
description = "Run Rust tests"
run = "cargo test --workspace"
[tasks."ts:test"]
description = "Run TypeScript tests"
raw = true
run = "bun test"
[tasks.test]
description = "Run all tests"
run = "mise rs:test ::: ts:test"I wrap all common scripts with mise convenience wrappers. This way I don't
have to think about package.json scripts, Rake tasks, or anything else. mise build and mise dev are muscle memory that's shared across all my projects.
Tasks can be namespaced with colons.
Complex tasks (more than 3 lines) go into .mise/tasks.
[tools]
age = "latest"
[settings]
age.key_file = "config/secret.key"
[env]
ADMIN_PASSWORD = { age = "abcdefghijklmnopqrstuvwxyzsecretthing" }
API_KEY = { age = "abcdefghijklmnopqrstuvwxyzsecretthing" }
SOME_SETTING = "false"mise replaces dotenv.
I use age to encrypt secrets which I then safely commit to version control. I
use this for both local development and production secrets, and create different
mise.production.toml and config/secrets/production.keys as necessary.
If config/secret.key isn't available, mise.local.toml can disable it:
[settings]
age.strict = false
disable_tools = ["age"]Or you can use env vars:
MISE_AGE_STRICT=false MISE_DISABLE_TOOLS=age mise buildI use a crates/ folder with {project}-{name} for packages.
I use defaults for fmt and clippy.
[tools]
"cargo:cargo-llvm-cov" = "latest"
jq = "latest"
[tasks."rs:test:coverage"]
description = "Run Rust tests with coverage summary"
run = "cargo llvm-cov --workspace --json | .mise/tasks/coverage-fmt".mise/tasks/coverage-fmt:
#!/bin/sh
# Reads cargo-llvm-cov JSON from stdin and prints a coverage table.
jq -r '
.data[0] as $d |
($d.files[0].filename | split("/crates/")[0] + "/crates/") as $prefix |
($d.files | map(.filename | ltrimstr($prefix) | length) | max + 3) as $w |
($d.files | map("\(.summary.lines.covered)/\(.summary.lines.count)" | length) | max + 3) as $lw |
def pad(n): . + ((n - length) * " ");
("-" * $w + "|" + "-" * $lw + "|" + "---------"),
(" File" | pad($w)) + "|" + (" Lines" | pad($lw)) + "| % Lines",
("-" * $w + "|" + "-" * $lw + "|" + "---------"),
(" All files" | pad($w)) + "|" + (" \($d.totals.lines.covered)/\($d.totals.lines.count)" | pad($lw)) + "| \($d.totals.lines.percent * 100 | round | . / 100)",
($d.files[] |
(.filename | ltrimstr($prefix)) as $name |
(" " + $name | pad($w)) + "|" + (" \(.summary.lines.covered)/\(.summary.lines.count)" | pad($lw)) + "| \(.summary.lines.percent * 100 | round | . / 100)"
),
("-" * $w + "|" + "-" * $lw + "|" + "---------")
'I like bun. It's fast and comes with most things I need, so I don't need to
install many deps.
{
"name": "project",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^6"
}
}This is the default package.json that bun init -y generates.
I will go to great lengths to not add anything to this file. No dependencies. This is in stark contrast to 99% of TypeScript projects.
Bun comes with a lot of things, and it's easy to forget and end up importing
@aws-sdk/client-s3 for no reason. A quick overview of what's built-in:
- HTTP server
- Testing
- JSX
- TypeScript
- fetch
- SQLite
- S3
- Redis
- JSONL
- YAML
- Markdown
- TOML
I will pull in a dependency if it's solving a big problem, like react, or
transformers.js.
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"incremental": true,
"tsBuildInfoFile": "./tmp/.tsbuildinfo"
}
}This is the default tsconfig.json that comes with bun init -y with the
following changes:
- Stricter flags flipped to true
- Incremental compilation enabled for a speed-up
- React removed
{
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"formatter": { "indentStyle": "space" },
"linter": {
"enabled": true,
"rules": { "complexity": { "useLiteralKeys": "off" } }
},
"files": { "includes": ["src/**"] },
"plugins": ["./config/biome.grit"]
}Default formatter settings except for spaces to indent as it's the prettier
default.
Default linter settings except for useLiteralKeys set to "off" because
it conflicts with tsconfig's noPropertyAccessFromIndexSignature.
config/biome.grit:
`mock.module($args)` as $call where {
register_diagnostic(
span = $call,
message = "mock.module() is banned — use dependency injection",
severity = "error"
)
}
mock.module() leaks across test files, so I ban it using a grit rule.
I use colocated tests, foo.ts -> foo.test.ts.
[tasks."ts:test:coverage"]
description = "Run TypeScript tests with coverage"
raw = true
run = "bun test --coverage"
[tasks."ts:test:slowest"]
description = "Show 10 slowest TypeScript tests"
raw = true
run = '''
bun test --reporter=junit \
--reporter-outfile=tmp/junit.xml && \
sed -n 's/.*<testcase name="\([^"]*\)".* time="\([^"]*\)".*/\2\t\1/p' \
tmp/junit.xml | sort -rn | head -10 \
| awk -F'\t' '{printf "%7.1fms %s\n",$1*1000,$2}'
'''I often measure what the top 10 slowest tests are. Over 10ms is usually a smell.
I always use raw = true for bun test as it improves output formatting.
I use claude as my main coding harness.
ln -s CLAUDE.md AGENTS.mdEnsures other harnesses auto-read the CLAUDE.md conventions.
.claude/hooks/session-start.sh:
#!/bin/bash
set -euo pipefail
[ "${CLAUDE_CODE_REMOTE:-}" = "true" ] || exit 0
if ! command -v mise &>/dev/null; then
curl -fsSL https://mise.run | sh
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
fi
mise trust --yes
mise install.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}Claude Code on web gets confused about how to set up mise properly.
claude plugin install rust-analyzer-lsp@claude-plugins-official --scope project{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "rustfmt $(jq -r '.tool_input.file_path') 2>/dev/null || true",
"if": "Edit(**/*.rs)|Write(**/*.rs)"
}
]
}
]
}
}claude plugin install typescript-lsp@claude-plugins-official --scope project[tools]
"npm:typescript-language-server" = "latest"The typescript-language-server is required in the path.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "biome format --write $(jq -r '.tool_input.file_path') 2>/dev/null || true",
"if": "Edit(src/**/*.ts)|Write(src/**/*.ts)"
}
]
}
]
}
}{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "fp=$(jq -r '.tool_input.file_path'); tf=\"${fp%.ts}\"; tf=\"${tf%.test}.test.ts\"; [ -f \"$tf\" ] && bun test \"$tf\" --bail || true",
"if": "Edit(src/**/*.ts)|Write(src/**/*.ts)"
}
]
}
]
}
}Derives the corresponding .test.ts file from whatever was edited (e.g. src/foo.ts → src/foo.test.ts, src/foo.test.ts → src/foo.test.ts) and runs it with --bail if it exists.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "[ \"$(git branch --show-current)\" = \"main\" ] && echo 'BLOCK: Do not commit to main. Create a feature branch first.' && exit 2 || exit 0",
"if": "Bash(git commit:*)"
}
]
}
]
}
}{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|Read",
"hooks": [
{
"type": "command",
"command": "echo 'BLOCK: Do not read or edit secret files' && exit 2",
"if": "Edit(*/secret.key)|Write(*/secret.key)|Read(*/secret.key)"
}
]
}
]
}
}claude plugin install superpowers@claude-plugins-official --scope project
claude plugin install playwright@claude-plugins-official --scope project
claude plugin install claude-code-setup@claude-plugins-official --scope project
claude plugin install claude-md-management@claude-plugins-official --scope project
claude plugin install feature-dev@claude-plugins-official --scope projectWhether I include these depends on the project.
.claude/settings.json:
{
"permissions": {
"allow": [
"Bash(gh issue list:*)",
"Bash(gh issue view:*)",
"Bash(gh pr checks:*)",
"Bash(gh pr create:*)",
"Bash(gh pr diff:*)",
"Bash(gh pr edit:*)",
"Bash(gh pr list:*)",
"Bash(gh pr view:*)",
"Bash(gh release:*)",
"Bash(gh repo view:*)",
"Bash(gh run list:*)",
"Bash(gh run view:*)",
"Bash(gh search:*)",
"Bash(git add:*)",
"Bash(git check-ignore:*)",
"Bash(git commit:*)",
"Bash(git fetch:*)",
"Bash(git log:*)",
"Bash(git push)",
"Bash(git rev-parse:*)",
"Bash(mise build:*)",
"Bash(mise bundle:*)",
"Bash(mise ci:*)",
"Bash(mise deploy:*)",
"Bash(mise dev:*)",
"Bash(mise format:*)",
"Bash(mise help:*)",
"Bash(mise install:*)",
"Bash(mise lint:*)",
"Bash(mise main:*)",
"Bash(mise tasks:*)",
"Bash(mise test:*)",
"Skill(claude-md-management:revise-claude-md)",
"WebSearch"
],
"deny": [
"Bash(env)",
"Bash(env :*)",
"Bash(export)",
"Bash(export :*)",
"Bash(mise env)",
"Bash(mise env :*)",
"Bash(printenv:*)",
"Bash(set)"
]
}
}This is a starter-pack allowing usually non-destructive actions, to pre-empt approval fatigue.
[tasks."macos:screenshot"]
description = "Take a screenshot of all displays"
run = '''
ts=$(date +%Y%m%d-%H%M%S)
screencapture -x "tmp/screen1-$ts.png" "tmp/screen2-$ts.png" "tmp/screen3-$ts.png"
echo "tmp/screen1-$ts.png tmp/screen2-$ts.png tmp/screen3-$ts.png"
'''
[tasks."linux:screenshot"]
description = "Take a screenshot"
run = '''
f="tmp/screenshot-$(date +%Y%m%d-%H%M%S).png"
grim "$f"
echo "$f"
'''
[tasks."linux:record"]
description = "Record screen for N seconds (default 5)"
usage = 'arg "[seconds]" default="5"'
run = '''
f="tmp/recording-$(date +%Y%m%d-%H%M%S).mp4"
timeout "$usage_seconds" wf-recorder -f "$f" -y 2>/dev/null || true
echo "$f"
'''These commands give Claude tools to take screenshots and visually inspect complex GUI apps.
I add this to CLAUDE.md:
## Commits
Use Conventional Commits, title is "what", body is "why":
- Check `git log -n 5` first to match existing style
- Don't use `--oneline`, commit bodies carry important context
- Subject ≤50 chars (including prefix): `feat: Add thing`
- Capitalize after prefix: `feat: Add thing` not `feat: add thing`
- Blank line, then 1-3 sentence description of "why", no bullet points
- Always `git add` and `git commit` as separate commands
## PRs
- Short essay (a few paragraphs) describing why the changes are needed
- Don't hard-wrap PR body, GitHub renders markdown with browser reflow[tools]
hk = "latest"
pkl = "latest"hk.pkl:
amends "package://github.com/jdx/hk/releases/download/v1.38.0/hk@1.38.0#/Config.pkl"
import "package://github.com/jdx/hk/releases/download/v1.38.0/hk@1.38.0#/Builtins.pkl"
hooks {
["commit-msg"] {
fix = true
steps {
["wrap-body"] {
stage = List()
fix = #"""
file="{{commit_msg_file}}"
subject=$(head -n 1 "$file")
body=$(tail -n +3 "$file" | grep -v '^#' || true)
if [ -z "$body" ]; then
exit 0
fi
wrapped=$(echo "$body" | fmt -w 72)
printf '%s\n\n%s\n' "$subject" "$wrapped" > "$file"
"""#
}
["conventional-commit"] = (Builtins.check_conventional_commit) {
depends = List("wrap-body")
}
["subject-length"] {
depends = List("wrap-body")
check = #"""
subject=$(head -n 1 "{{commit_msg_file}}")
len=$(printf '%s' "$subject" | wc -c | tr -d ' ')
if [ "$len" -gt 50 ]; then
echo "Subject is $len chars (max 50): $subject" >&2
exit 1
fi
"""#
}
}
}
}hk installThis sets up hk to check that commits follow the CLAUDE.md guidelines.
.claude/*.local.*
.playwright-mcp
.claude/worktrees
config/secret.key
coverage
mise.local.toml
tmp/*
!tmp/.keep[tasks.main]
description = "Switch to main, pull, and clean up merged branches"
run = '''
set -e
git checkout main
git pull --rebase
git fetch --prune
cleaned=0
for branch in $(git branch -vv | grep ': gone]' | sed 's/^[*+] / /' | awk '{print $1}'); do
git branch -D "$branch"
cleaned=$((cleaned + 1))
done
if [ "$cleaned" -gt 0 ]; then
echo "Cleaned up ${cleaned} merged branch(es)"
else
echo "No stale branches to clean up"
fi
'''I run mise main after merging a piece of work.
.claude/hooks/worktree-setup.sh:
#!/bin/bash
set -euo pipefail
INPUT=$(cat)
NAME=$(echo "$INPUT" | jq -r '.name')
CWD=$(echo "$INPUT" | jq -r '.cwd')
WORKTREE="$CWD/.claude/worktrees/$NAME"
git worktree add -B "worktree-$NAME" "$WORKTREE" HEAD >&2
ln -sf "$CWD/config/secret.key" "$WORKTREE/config/secret.key"
mkdir -p "$WORKTREE/tmp"
cd "$WORKTREE"
mise trust --yes >&2
mise install >&2
echo "$WORKTREE".claude/hooks/worktree-teardown.sh:
#!/bin/bash
set -euo pipefail
INPUT=$(cat)
WORKTREE=$(echo "$INPUT" | jq -r '.worktree_path')
NAME=$(basename "$WORKTREE")
REPO=$(cd "$WORKTREE" && git rev-parse --path-format=absolute --git-common-dir)
REPO=$(dirname "$REPO")
cd "$REPO"
git worktree remove "$WORKTREE" --force >&2
git branch -D "worktree-$NAME" >&2 || true
echo "$WORKTREE".claude/settings.json:
{
"hooks": {
"WorktreeCreate": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/worktree-setup.sh",
"timeout": 120,
"statusMessage": "Setting up worktree..."
}
]
}
],
"WorktreeRemove": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/worktree-teardown.sh",
"statusMessage": "Removing worktree..."
}
]
}
]
}
}Ensures that secrets are copied, and mise is set up.
[tools]
"gem:kamal" = "latest"Example single-server deploy with local docker registry and a pgvector
database:
config/deploy.yml:
service: foo
servers:
web:
- foo.com
proxy:
ssl: true
host: foo.com
registry:
server: "localhost:5555"
env:
secret:
- RAILS_MASTER_KEY
clear:
WEB_CONCURRENCY: 2
builder:
arch: amd64
accessories:
pg18:
image: pgvector/pgvector:pg18
host: foo.com
env:
clear:
POSTGRES_USER: foo
secret:
- POSTGRES_PASSWORD
directories:
- pg18:/var/lib/postgresql/18/docker.kamal/secrets daisy-chains encrypted secrets from mise, which is why it's
important to run kamal commands via mise wrappers:
RAILS_MASTER_KEY=$RAILS_MASTER_KEY
POSTGRES_PASSWORD=$POSTGRES_PASSWORD[tasks.deploy]
description = "Deploy via Kamal"
raw = true
run = "kamal deploy"
[tasks."ops:setup"]
description = "First-time Kamal setup"
raw = true
run = "kamal setup"
[tasks."ops:logs"]
description = "Tail logs from deployment"
raw = true
usage = '''
flag "-f --follow"
arg "[lines]" default="50"
'''
run = 'kamal app logs --lines $usage_lines ${usage_follow:+-f}'
[tasks."ops:console"]
description = "Open a console in the container"
raw = true
run = "kamal app exec --interactive --reuse 'bash'"
[tasks."ops:restart"]
description = "Restart the app container"
raw = true
run = "kamal app boot".github/dependabot.yml:
version: 2
updates:
- package-ecosystem: <bun|cargo|github-actions|docker|bundler> # Pick one
directory: /
schedule:
interval: weekly
day: monday
commit-message:
prefix: "chore(deps):"
groups:
minor-and-patch:
update-types:
- minor
- patchThis ensures dependabot groups updates into one PR instead of many.
[tools]
gh = "latest"
[tasks.ci]
description = "Run all CI checks in parallel"
depends = ["typecheck", "test", "format", "lint"]
run = "gh signoff ci 2>/dev/null || true"gh extension install basecamp/gh-signoff
gh signoff installThis runs CI locally and signs off on GitHub using basecamp/gh-signoff.
docker-compose.yml:
services:
pg18:
image: pgvector/pgvector:pg18
ports:
- "${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: myapp
volumes:
- ./storage/pg18:/var/lib/postgresql/18/docker
healthcheck:
test: ["CMD", "pg_isready", "-U", "myapp"]
interval: 100ms
timeout: 500ms
retries: 30I prefer running things like Postgres or Redis via docker containers instead of
mise tools.
[tasks.dev]
description = "Start development server"
depends = ["pg:up"]
run = "bin/setup"
[tasks."pg:up"]
description = "Start PostgreSQL if not running"
run = "docker compose up -d pg18 --wait"
[tasks."pg:down"]
description = "Stop PostgreSQL"
run = "docker compose down"
[tasks."pg:logs"]
description = "Show PostgreSQL logs"
run = "docker compose logs -f pg18"
[tasks."pg:psql"]
description = "Open PostgreSQL console"
run = "docker compose exec pg18 psql -U myapp -d myapp_development"
[tasks."pg:dump"]
description = "Dump main development database"
run = '''
docker compose exec pg18 pg_dump -U myapp -Fc myapp_development > backup.dump
'''
[tasks."pg:restore"]
description = "Restore local database from backup file"
usage = 'arg "<backup_file>"'
run = '''
docker compose exec -T pg18 pg_restore -U myapp -d myapp_development \
--clean --if-exists < {{arg(name="backup_file")}}
'''