Create Your First Custom Skill for Hermes Agent
Why Build Custom Skills?
While the community skill catalog covers many use cases, sometimes you need something specific to your workflow. Custom skills let you:
- Integrate with internal company APIs
- Automate domain-specific tasks
- Connect to niche tools and services
- Create reusable workflows for your team
Skill Architecture
A Hermes skill is a self-contained module with a standard interface:
my-skill/
βββ index.ts # Main skill file
βββ package.json # Metadata and dependencies
βββ README.md # Documentation
βββ test/
βββ index.test.ts # Tests
Step 1: Scaffold a New Skill
hermes skill create my-weather-skill
π¦ Creating skill: my-weather-skill
Directory: ~/.hermes/skills/my-weather-skill/
Created files:
βββ index.ts
βββ package.json
βββ README.md
β
Skill scaffolded! Edit index.ts to get started.
Step 2: Define the Skill
Open index.ts and define your skill:
import type { HermesSkill, SkillContext, SkillResult } from 'hermes-agent/skills';,const weatherSkill: HermesSkill = {
// Metadata
name: 'weather-lookup',
version: '1.0.0',
description: 'Get current weather and forecasts for any city',
author: 'Your Name',
// Parameter schema β tells Hermes what inputs the skill needs
parameters: {
city: {
type: 'string',
required: true,
description: 'City name (e.g., "Tokyo", "New York")',
},
units: {
type: 'string',
required: false,
default: 'metric',
enum: ['metric', 'imperial'],
description: 'Temperature units',
},
},
// Permissions this skill requires
permissions: ['network'],
// The main execution function
async execute(
params: { city: string; units?: string },
context: SkillContext
): Promise
{ const { city, units = 'metric' } = params;
try {
// Call weather API
const apiKey = context.config.get('weather_api_key');
if (!apiKey) {
return {
success: false,
error: 'Weather API key not configured. Run: hermes skill configure weather-lookup',
};
}
const url =
https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units}&appid=${apiKey};const response = await fetch(url);
if (!response.ok) {
return {
success: false,
error:
City "${city}" not found. Check the spelling and try again.,};
}
const data = await response.json();
const tempUnit = units === 'metric' ? 'Β°C' : 'Β°F';
return {
success: true,
output:
Weather in ${data.name}, ${data.sys.country}:π‘οΈ Temperature: ${data.main.temp}${tempUnit} (feels like ${data.main.feels_like}${tempUnit})
π§ Humidity: ${data.main.humidity}%
π€οΈ Conditions: ${data.weather[0].description}
π¨ Wind: ${data.wind.speed} ${units === 'metric' ? 'm/s' : 'mph'}
data: data, // Raw data for other skills to use
};
} catch (error) {
return {
success: false,
error:
Failed to fetch weather: ${error.message},};
}
},
// Optional: configuration setup
async configure(context: SkillContext): Promise
{ const apiKey = await context.prompt('Enter your OpenWeatherMap API key:');
context.config.set('weather_api_key', apiKey);
context.log('β API key saved!');
},
};
export default weatherSkill;
Step 3: Understanding the Skill API
SkillContext
The context object gives your skill access to Hermes internals:
interface SkillContext {
// Configuration storage (persisted)
config: {
get(key: string): string | undefined;
set(key: string, value: string): void;
};
// Logging
log(message: string): void;
warn(message: string): void;
error(message: string): void;
// User interaction
prompt(message: string): Promise;
// Agent capabilities
agent: {
ask(question: string): Promise; // Ask the AI model
memory: MemoryStore; // Long-term memory
session: SessionStore; // Session data
};
// File system (sandboxed)
fs: {
read(path: string): Promise;
write(path: string, content: string): Promise;
exists(path: string): Promise;
};
}
SkillResult
Every skill execution returns a SkillResult:
interface SkillResult {
success: boolean;
output?: string; // Human-readable output shown to the user
data?: any; // Structured data for other skills or further processing
error?: string; // Error message if success is false
}
Step 4: Test Your Skill
Manual Testing
# Test the skill directly
hermes skill test weather-lookup --params '{"city": "Tokyo"}'
π§ͺ Testing skill: weather-lookup
Input: { city: "Tokyo" }
Output:
Weather in Tokyo, JP:
π‘οΈ Temperature: 22Β°C (feels like 21Β°C)
π§ Humidity: 65%
π€οΈ Conditions: scattered clouds
π¨ Wind: 3.6 m/s
β
Test passed! (1.2s)
Automated Tests
Create test/index.test.ts:
import { describe, it, expect } from 'vitest';
import weatherSkill from '../index';
describe('weather-lookup', () => {
it('should return weather for a valid city', async () => {
const mockContext = createMockContext({
config: { weather_api_key: 'test-key' },
});
const result = await weatherSkill.execute(
{ city: 'Tokyo' },
mockContext
);
expect(result.success).toBe(true);
expect(result.output).toContain('Tokyo');
});
it('should fail gracefully for invalid cities', async () => {
const mockContext = createMockContext({
config: { weather_api_key: 'test-key' },
});
const result = await weatherSkill.execute(
{ city: 'NotARealCity12345' },
mockContext
);
expect(result.success).toBe(false);
expect(result.error).toContain('not found');
});
it('should require API key configuration', async () => {
const mockContext = createMockContext({ config: {} });
const result = await weatherSkill.execute(
{ city: 'Tokyo' },
mockContext
);
expect(result.success).toBe(false);
expect(result.error).toContain('not configured');
});
});
Run tests:
cd ~/.hermes/skills/my-weather-skill
npm test
Step 5: Install and Use
Local Installation
hermes skill install --local ~/.hermes/skills/my-weather-skill
Now use it naturally in conversation:
> What's the weather in Paris?
π§ Using skill: weather-lookup
Weather in Paris, FR:
π‘οΈ Temperature: 18Β°C (feels like 17Β°C)
π§ Humidity: 72%
π€οΈ Conditions: light rain
π¨ Wind: 4.2 m/s
Publishing to the Community
When your skill is ready, publish it:
hermes skill publish
π¦ Publishing weather-lookup v1.0.0...
Checklist:
β
package.json valid
β
README.md present
β
Tests passing (3/3)
β
No security issues
β
Permissions declared
Published! Your skill is now available at:
https://skills.hermesagent.sbs/weather-lookup
Best Practices
Error Handling
Always return meaningful error messages:
// β Bad
return { success: false, error: 'Error' };
// β
Good
return {
success: false,
error: Failed to connect to ${apiUrl}. Check your internet connection and API key.,
};
Graceful Degradation
If part of the skill fails, return partial results:
const weather = await getWeather(city); // Succeeded
const forecast = await getForecast(city); // Failed
return {
success: true,
output: Current: ${weather}\n\nβ οΈ Forecast unavailable (API limit reached),
};
Security
- Never hardcode API keys β use
context.config - Validate all inputs before using them
- Declare all required permissions
- Sanitize file paths to prevent directory traversal
Next Steps
- Browse community skills β See what others have built
- Skill best practices β Production-quality patterns
- The self-learning loop β How Hermes learns new skills automatically
Last updated: April 17, 2026 Β· Hermes Agent v0.8