Write consistent, actionable validation error messages in gh-aw.
npx skills add https://github.com/github/gh-aw --skill error-messages
Use this format for gh-aw validation errors. Keep messages clear, actionable, and example-driven.
[what's wrong]. [what's expected]. [example of correct usage]
Make each error message answer three questions:
Avoid standalone negative wording. Pair it with expected behavior and a concrete fix.
| Avoid only-negative wording | Prefer constructive wording |
|---|---|
| invalid | expected + valid format/options |
| cannot | requires + precondition |
| must | should + example |
| failed | action context + recovery step |
❌ invalid repo format: %s
✅ invalid repo format '%s' — expected 'owner/repo' format (for example: 'github/gh-aw')
❌ not in a git repository
✅ not in a git repository — run 'git init' or 'cd' to a git repository
NewValidationError vs fmt.ErrorfNewValidationError(field, value, reason, suggestion) in *_validation.go logic.field for the exact config pathreason for what failedsuggestion for an actionable fix with an examplefmt.Errorf for operational/wrapping errors (%w) where you are propagating a lower-level failure with context.fmt.Errorf("failed to X: %w", err) unless you add recovery guidance.Every suggestion should:
Example:
Use one supported engine.
✓ Example:
engine: copilot
✗ Avoid:
engine: unknown
These examples follow the template and provide actionable guidance:
return nil, fmt.Errorf("invalid time delta format: +%s. Expected format like +25h, +3d, +1w, +1mo, +1d12h30m", deltaStr)
✅ Why it's good:
return "", fmt.Errorf("manual-approval value must be a string, got %T. Example: manual-approval: \"production\"", val)
✅ Why it's good:
return fmt.Errorf("invalid engine: %s. Valid engines are: copilot, claude, codex, custom. Example: engine: copilot", engineID)
✅ Why it's good:
return fmt.Errorf("tool '%s' mcp configuration must specify either 'command' or 'container'. Example:\ntools:\n %s:\n command: \"npx @my/tool\"", toolName, toolName)
✅ Why it's good:
These examples lack clarity or actionable guidance:
return fmt.Errorf("invalid format")
❌ Problems:
return fmt.Errorf("manual-approval value must be a string")
❌ Problems:
return fmt.Errorf("invalid engine: %s", engineID)
❌ Problems:
Always include examples for:
fmt.Errorf("invalid date format. Expected: YYYY-MM-DD HH:MM:SS. Example: 2024-01-15 14:30:00")
fmt.Errorf("invalid permission level: %s. Valid levels: read, write, none. Example: permissions:\n contents: read", level)
fmt.Errorf("timeout-minutes must be an integer, got %T. Example: timeout-minutes: 10", value)
fmt.Errorf("invalid MCP server config. Example:\nmcp-servers:\n my-server:\n command: \"node\"\n args: [\"server.js\"]")
Examples can be omitted when:
return fmt.Errorf("failed to parse configuration: %w", err)
return fmt.Errorf("duplicate unit '%s' in time delta: +%s", unit, deltaStr)
return fmt.Errorf("unsupported feature. See https://docs.example.com/features")
%s - strings%d - integers%T - type of value%v - general value%w - wrapped errorsFor YAML configuration examples spanning multiple lines:
fmt.Errorf("invalid config. Example:\ntools:\n github:\n mode: \"remote\"")
Use proper YAML syntax in examples:
// Good - shows quotes when needed
fmt.Errorf("Example: name: \"my-workflow\"")
// Good - shows no quotes for simple values
fmt.Errorf("Example: timeout-minutes: 10")
Use the same field names as in YAML:
// Good - matches YAML field name
fmt.Errorf("timeout-minutes must be positive")
// Bad - uses different name
fmt.Errorf("timeout must be positive")
All improved error messages should have corresponding tests:
func TestErrorMessageQuality(t *testing.T) {
err := validateSomething(invalidInput)
require.Error(t, err)
// Error should explain what's wrong
assert.Contains(t, err.Error(), "invalid")
// Error should include expected format or values
assert.Contains(t, err.Error(), "Expected")
// Error should include example
assert.Contains(t, err.Error(), "Example:")
}
When improving existing error messages:
// Time deltas
fmt.Errorf("invalid time delta format: +%s. Expected format like +25h, +3d, +1w, +1mo, +1d12h30m", input)
// Dates
fmt.Errorf("invalid date format: %s. Expected: YYYY-MM-DD or relative like -1w. Example: 2024-01-15 or -7d", input)
// URLs
fmt.Errorf("invalid URL format: %s. Expected: https:// URL. Example: https://api.example.com", input)
// Boolean expected
fmt.Errorf("read-only must be a boolean, got %T. Example: read-only: true", value)
// String expected
fmt.Errorf("workflow name must be a string, got %T. Example: name: \"my-workflow\"", value)
// Object expected
fmt.Errorf("permissions must be an object, got %T. Example: permissions:\n contents: read", value)
// Engine selection
fmt.Errorf("invalid engine: %s. Valid engines: copilot, claude, codex, custom. Example: engine: copilot", id)
// Permission levels
fmt.Errorf("invalid permission level: %s. Valid levels: read, write, none. Example: contents: read", level)
// Tool modes
fmt.Errorf("invalid mode: %s. Valid modes: local, remote. Example: mode: \"remote\"", mode)
// Missing required field
fmt.Errorf("tool '%s' missing required 'command' field. Example:\ntools:\n %s:\n command: \"node server.js\"", name, name)
// Mutually exclusive fields
fmt.Errorf("cannot specify both 'command' and 'container'. Choose one. Example: command: \"node server.js\"")
// Invalid combination
fmt.Errorf("http MCP servers cannot use 'container' field. Example:\ntools:\n my-http:\n type: http\n url: \"https://api.example.com\"")
pkg/workflow/time_delta.gopkg/workflow/*_test.goWhen writing error messages, consider:
Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Query the CELLxGENE Census (61M+ cells) programmatically. Use when you need expression data across tissues, diseases, or cell types from the largest curated single-cell atlas. Best for population-scale queries, reference atlas comparisons. For analyzing your own data use scanpy or scvi-tools.
Take github/error-messages from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.