encore-go-code-review — Claude Code skill: Encore Go review
A skill that scans existing Encore Go code for anti-patterns: infrastructure inside functions, missing context, SQL injection risks before you merge.
npx skills add encoredev/skills --skill encore-go-code-review -a claude-code
npx skills add encoredev/skills --skill go-code-review -a claude-codeWhat encore-go-code-review does
The skill fires when you review a pull request, audit existing code, or check for Encore-specific anti-patterns before merging. Greenfield code being actively written is out of scope.
Inside is a checklist of nine issues, each with a wrong and a correct example. Critical ones: infrastructure declared inside a function instead of at package level, an API endpoint missing context.Context as its first parameter, SQL built with fmt.Sprintf instead of $1 parameters, response types returned by value instead of pointer, ignored errors. Warnings cover unchecked sqldb.ErrNoRows, public internal endpoints, non-idempotent subscription handlers, and query rows left unclosed without defer rows.Close().
Findings come back as [CRITICAL], [WARNING] and [GOOD] lines with file and line. This is Encore Go specifically, so checks like errs.B and sqldb.Query won't mean anything outside that stack.
What it helps with
- catch infrastructure declared inside functions and move it to package level
- spot SQL injection from string interpolation instead of parameterized queries
- verify every API endpoint takes
context.Contextas its first parameter - find a non-idempotent subscription handler before it charges a customer twice
- make sure query rows are closed with
defer rows.Close()
Example prompts for the agent
- review this PR for Encore anti-patterns
- check the code before merge: infrastructure, context, SQL
- audit this package for code smells in Encore Go
How to install encore-go-code-review
1Skills CLI (Vercel)
npx skills add encoredev/skills --skill encore-go-code-review -a claude-codenpx skills add encoredev/skills --skill go-code-review -a claude-code2Manually: put the files in place
~/.claude/skills/go-code-review/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/go-code-review/
1Skills CLI (Vercel)
npx skills add encoredev/skills --skill go-code-review -a codex2Manually: put the files in place
.agents/skills/go-code-review/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add encoredev/skills --skill go-code-review -a cursor2Manually: put the files in place
.cursor/skills/go-code-review/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install encoredev/skills go-code-review2Manually: put the files in place
.github/skills/go-code-review/SKILL.md1Skills CLI (Vercel)
npx skills add encoredev/skills --skill go-code-review -a gemini-cli2Platform CLI
gemini skills install https://github.com/encoredev/skills --path encore/go-code-reviewManually: put the files in place
.devin/skills/go-code-review/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add encoredev/skills --skill go-code-review -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about encore-go-code-review
How do I install encore-go-code-review in Claude?
Run npx skills add encoredev/skills --skill encore-go-code-review -a claude-code. The skill file then lives at ~/.claude/skills/go-code-review/SKILL.md.
What does encore-go-code-review do?
It runs existing Encore Go code through a nine-point anti-pattern checklist and reports findings as [CRITICAL], [WARNING] and [GOOD] lines.
When should I skip this skill?
On greenfield code you are still writing. It is built for reviewing and auditing code that already exists.
Source text
SKILL.md · Apache-2.0
---
name: encore-go-code-review
description: Review existing Encore Go code for best practices and common anti-patterns.
when_to_use: >-
User is reviewing a pull request, auditing existing code, or checking for Encore-specific anti-patterns before merging — infrastructure declared inside functions, missing service files, wrong import paths, raw `errors.New(...)` thrown instead of `errs.B`, untyped APIs, panicking in handlers. SKIP for greenfield code being actively written. Trigger phrases: "audit", "review", "before merge", "PR review", "anti-patterns", "code smell", "lint this".
---
# Encore Go Code Review
## Instructions
When reviewing Encore Go code, check for these common issues:
## Critical Issues
### 1. Infrastructure Inside Functions
```go
// WRONG: Infrastructure declared inside function
func setup() {
db := sqldb.NewDatabase("mydb", sqldb.DatabaseConfig{...})
topic := pubsub.NewTopic[*Event]("events", pubsub.TopicConfig{...})
}
// CORRECT: Package level declaration
var db = sqldb.NewDatabase("mydb", sqldb.DatabaseConfig{
Migrations: "./migrations",
})
var topic = pubsub.NewTopic[*Event]("events", pubsub.TopicConfig{
DeliveryGuarantee: pubsub.AtLeastOnce,
})
```
### 2. Missing Context Parameter
```go
// WRONG: Missing context
//encore:api public method=GET path=/users/:id
func GetUser(params *GetUserParams) (*User, error) {
// ...
}
// CORRECT: Context as first parameter
//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
// ...
}
```
### 3. SQL Injection Risk
```go
// WRONG: String interpolation
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
rows, err := db.Query(ctx, query)
// CORRECT: Parameterized query
rows, err := sqldb.Query[User](ctx, db, `
SELECT * FROM users WHERE email = $1
`, email)
```
### 4. Wrong Return Types
```go
// WRONG: Returning non-pointer struct
//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (User, error) {
// ...
}
// CORRECT: Return pointer to struct
//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
// ...
}
```
### 5. Ignoring Errors
```go
// WRONG: Ignoring error
user, _ := sqldb.QueryRow[User](ctx, db, query, id)
// CORRECT: Handle error
user, err := sqldb.QueryRow[User](ctx, db, query, id)
if err != nil {
return nil, err
}
```
## Warning Issues
### 6. Not Checking for ErrNoRows
```go
// RISKY: Returns nil without proper error
func getUser(ctx context.Context, id string) (*User, error) {
user, err := sqldb.QueryRow[User](ctx, db, `
SELECT * FROM users WHERE id = $1
`, id)
if err != nil {
return nil, err // ErrNoRows returns generic error
}
return user, nil
}
// BETTER: Check for not found specifically
import "errors"
func getUser(ctx context.Context, id string) (*User, error) {
user, err := sqldb.QueryRow[User](ctx, db, `
SELECT * FROM users WHERE id = $1
`, id)
if errors.Is(err, sqldb.ErrNoRows) {
return nil, &errs.Error{
Code: errs.NotFound,
Message: "user not found",
}
}
if err != nil {
return nil, err
}
return user, nil
}
```
### 7. Public Internal Endpoints
```go
// CHECK: Should this cron endpoint be public?
//encore:api public method=POST path=/internal/cleanup
func CleanupJob(ctx context.Context) error {
// ...
}
// BETTER: Use private for internal endpoints
//encore:api private
func CleanupJob(ctx context.Context) error {
// ...
}
```
### 8. Non-Idempotent Subscription Handlers
```go
// RISKY: Not idempotent (pubsub has at-least-once delivery)
var _ = pubsub.NewSubscription(OrderCreated, "process-order",
pubsub.SubscriptionConfig[*OrderCreatedEvent]{
Handler: func(ctx context.Context, event *OrderCreatedEvent) error {
return chargeCustomer(ctx, event.OrderID) // Could charge twice!
},
},
)
// SAFER: Check before processing
var _ = pubsub.NewSubscription(OrderCreated, "process-order",
pubsub.SubscriptionConfig[*OrderCreatedEvent]{
Handler: func(ctx context.Context, event *OrderCreatedEvent) error {
order, err := getOrder(ctx, event.OrderID)
if err != nil {
return err
}
if order.Status != "pending" {
return nil // Already processed
}
return chargeCustomer(ctx, event.OrderID)
},
},
)
```
### 9. Not Closing Query Rows
```go
// WRONG: Rows not closed
func listUsers(ctx context.Context) ([]*User, error) {
rows, err := sqldb.Query[User](ctx, db, `SELECT * FROM users`)
if err != nil {
return nil, err
}
// Missing: defer rows.Close()
var users []*User
for rows.Next() {
users = append(users, rows.Value())
}
return users, nil
}
// CORRECT: Always close rows
func listUsers(ctx context.Context) ([]*User, error) {
rows, err := sqldb.Query[User](ctx, db, `SELECT * FROM users`)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
users = append(users, rows.Value())
}
return users, rows.Err()
}
```
## Review Checklist
- [ ] All infrastructure at package level
- [ ] All API endpoints have `context.Context` as first parameter
- [ ] SQL uses parameterized queries (`$1`, `$2`, etc.)
- [ ] Response types are pointers
- [ ] Errors are handled, not ignored
- [ ] `sqldb.ErrNoRows` checked where appropriate
- [ ] Internal endpoints use `private` not `public`
- [ ] Subscription handlers are idempotent
- [ ] Query rows are closed with `defer rows.Close()`
- [ ] Migrations follow naming convention (`1_name.up.sql`)
## Output Format
When reviewing, report issues as:
```
[CRITICAL] [file:line] Description of issue
[WARNING] [file:line] Description of concern
[GOOD] Notable good practice observed
```
© encoredev · Apache-2.0 · encoredev/skills