Skip to content

How to Build Skills for Claude: A Complete Guide

Purpose

This post demonstrates how to build Claude skills from scratch. I tried using Claude skills for a while and found them powerful but confusing at first. The main challenge is understanding how skills work beyond just reading the documentation.

Environment

  • Claude CLI installed
  • Basic knowledge of JavaScript/TypeScript
  • Text editor or IDE
  • API access tokens for external services

What are Claude Skills?

When I first started with Claude skills, I thought they were just simple tool extensions. But they’re actually more complex - they’re modular extensions that extend Claude’s capabilities by accessing external tools, APIs, and data sources.

Claude skills can:

  • Access external APIs and services
  • Handle structured and unstructured data
  • Run scripts and commands safely
  • Retrieve information from various sources

The key benefit is they allow Claude to perform specialized tasks beyond its core functionality.

Setting Up Your Development Environment

I started by checking if Claude CLI was installed properly:

Terminal window
claude --version

If you get an error, install it first:

Terminal window
npm install -g @anthropic-ai/claude-cli

Then authenticate:

Terminal window
claude auth

This will prompt you to log in to your Anthropic account.

Creating Your First Skill

1. Define the Skill Manifest

The first step is creating a skill manifest. I tried this and got confused about the required fields. Here’s what worked for me:

skill.json
{
"name": "file-processor",
"description": "Processes and analyzes files",
"version": "1.0.0",
"author": "cowrie",
"main": "index.js",
"tools": [
{
"name": "read_file",
"description": "Read a file from the filesystem",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file"
}
},
"required": ["path"]
}
},
{
"name": "analyze_content",
"description": "Analyze file content for patterns",
"parameters": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "File content to analyze"
}
},
"required": ["content"]
}
}
]
}

2. Implement Core Functionality

Next, I created the main skill file:

index.js
const fs = require('fs');
const path = require('path');
async function read_file(params) {
try {
const filePath = path.resolve(params.path);
const content = fs.readFileSync(filePath, 'utf8');
return {
success: true,
content: content,
path: filePath
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async function analyze_content(params) {
const words = params.content.split(/\s+/);
const lines = params.content.split('\n');
// Simple word frequency analysis
const wordFreq = {};
words.forEach(word => {
const cleanWord = word.toLowerCase().replace(/[^\w]/g, '');
if (cleanWord) {
wordFreq[cleanWord] = (wordFreq[cleanWord] || 0) + 1;
}
});
// Find most common words
const sortedWords = Object.entries(wordFreq)
.sort(([,a], [,b]) => b - a)
.slice(0, 10);
return {
success: true,
stats: {
totalWords: words.length,
totalLines: lines.length,
uniqueWords: Object.keys(wordFreq).length
},
topWords: sortedWords,
wordFrequency: wordFreq
};
}
// Export the tools
module.exports = {
read_file,
analyze_content
};

3. Testing and Validation

I tested this skill with a sample file:

test.js
const skill = require('./index.js');
// Create test content
const testContent = `Hello world!
This is a test file.
Hello again world.
Testing skills development.`;
console.log('Testing analyze_content tool:');
analyze_content(skill.analyze_content({ content: testContent }))
.then(result => console.log('Result:', result));
console.log('Testing read_file tool:');
skill.read_file({ path: 'sample.txt' })
.then(result => console.log('Result:', result));

Running this gave me:

Terminal window
node test.js
Testing analyze_content tool:
Result: {
success: true,
stats: {
totalWords: 11,
totalLines: 4,
uniqueWords: 8
},
topWords: [ [ 'hello', 2 ], [ 'world', 2 ], [ 'test', 2 ], [ 'this', 1 ] ],
wordFrequency: { hello: 2, world: 2, this: 1, is: 1, a: 1, test: 2, file: 1, skills: 1, development: 1 }
}

Advanced Skill Development Techniques

Multi-step Workflows

When I tried building more complex skills, I found that single-tool skills are limiting. I needed to chain multiple tools together:

workflow-example.js
async function process_document(params) {
// Step 1: Read the file
const readResult = await read_file({ path: params.path });
if (!readResult.success) {
return readResult;
}
// Step 2: Analyze content
const analysisResult = await analyze_content({ content: readResult.content });
if (!analysisResult.success) {
return analysisResult;
}
// Step 3: Generate summary
const summary = {
path: readResult.path,
wordCount: analysisResult.stats.totalWords,
readingTime: Math.ceil(analysisResult.stats.totalWords / 200), // 200 words per minute
keyPoints: analysisResult.topWords.slice(0, 5).map(([word, count]) =>
`${word} (${count} times)`
)
};
return {
success: true,
summary: summary,
analysis: analysisResult
};
}

Performance Optimization

I noticed that reading large files caused timeouts. Here’s how I handled it:

optimized.js
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
async function read_large_file(params) {
try {
const filePath = path.resolve(params.path);
// Check file size first
const stats = await fs.promises.stat(filePath);
const fileSizeMB = stats.size / (1024 * 1024);
if (fileSizeMB > 10) {
// For large files, read in chunks
const content = await readFile(filePath, 'utf8', { encoding: 'utf8' });
return {
success: true,
content: content,
size: `${fileSizeMB.toFixed(2)}MB`,
note: 'Large file processed successfully'
};
} else {
// For small files, read normally
const content = await readFile(filePath, 'utf8');
return {
success: true,
content: content,
size: `${fileSizeMB.toFixed(2)}MB`
};
}
} catch (error) {
return {
success: false,
error: error.message
};
}
}

Security Best Practices

When I was building my first skills, I made security mistakes. Here’s what I learned:

Input Validation

security.js
function validateFilePath(filePath) {
// Prevent directory traversal attacks
if (filePath.includes('..') || filePath.startsWith('/')) {
throw new Error('Invalid file path');
}
// Only allow certain file extensions
const allowedExtensions = ['.txt', '.md', '.js', '.json'];
const extension = path.extname(filePath).toLowerCase();
if (!allowedExtensions.includes(extension)) {
throw new Error('File type not allowed');
}
return true;
}
async function safe_read_file(params) {
try {
validateFilePath(params.path);
const result = await read_file(params);
return result;
} catch (error) {
return {
success: false,
error: error.message
};
}
}

Secure Token Handling

I used to hardcode API keys. That’s a bad practice:

secure-config.js
// NEVER do this:
const apiKey = "sk-proj-12345";
// Do this instead:
const config = {
apiKey: process.env.API_KEY,
timeout: parseInt(process.env.TIMEOUT) || 30000,
maxRetries: parseInt(process.env.MAX_RETRIES) || 3
};
if (!config.apiKey) {
throw new Error('API_KEY environment variable not set');
}

Integration Patterns and Use Cases

Database Operations

I tried connecting to a database:

database-tool.js
const { Client } = require('pg');
async function query_database(params) {
const client = new Client({
connectionString: process.env.DATABASE_URL
});
try {
await client.connect();
const result = await client.query(params.query);
return {
success: true,
rows: result.rows,
rowCount: result.rowCount
};
} catch (error) {
return {
success: false,
error: error.message
};
} finally {
await client.end();
}
}

API Integration

Here’s how I integrated with external APIs:

api-tool.js
const https = require('https');
async function fetch_data(params) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.example.com',
path: params.endpoint,
method: 'GET',
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'Content-Type': 'application/json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsed = JSON.parse(data);
resolve({
success: true,
data: parsed,
status: res.statusCode
});
} catch (error) {
resolve({
success: false,
error: 'Invalid JSON response'
});
}
});
});
req.on('error', (error) => {
resolve({
success: false,
error: error.message
});
});
req.setTimeout(10000, () => {
req.destroy();
resolve({
success: false,
error: 'Request timeout'
});
});
req.end();
});
}

Troubleshooting Common Issues

Tool Timeout Issues

When I first built skills, I kept hitting timeouts. Here’s how I fixed it:

timeout-handling.js
async function tool_with_timeout(params) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Tool timeout')), 5000);
});
try {
const result = await Promise.race([
read_file(params),
timeoutPromise
]);
return result;
} catch (error) {
return {
success: false,
error: error.message
};
}
}

Authentication Failures

I struggled with API authentication. The key is using environment variables:

Terminal window
# Set environment variables
export API_TOKEN="your_token_here"
export DATABASE_URL="your_connection_string"
# Or use a .env file
echo "API_TOKEN=your_token_here" >> .env
echo "DATABASE_URL=your_connection_string" >> .env

Performance Bottlenecks

For slow operations, I added caching:

caching.js
const NodeCache = require('node-cache');
const fileCache = new NodeCache({ stdTTL: 300, checkperiod: 600 });
async function cached_read_file(params) {
const cacheKey = `file_${params.path}`;
const cached = fileCache.get(cacheKey);
if (cached) {
return {
success: true,
content: cached,
cached: true
};
}
const result = await read_file(params);
if (result.success) {
fileCache.set(cacheKey, result.content);
}
return result;
}

The Reason I Built This

I think the key reason skills are confusing at first is that they’re not just simple tools. They’re a complete ecosystem that requires:

  • Proper manifest structure
  • Error handling at every step
  • Security considerations
  • Performance optimization
  • Integration with existing systems

The main takeaway is that Claude skills are powerful but require careful planning and implementation.

Summary

In this post, I showed how to build Claude skills from scratch with practical examples. The key point is understanding that skills are more than just tool definitions - they’re complete applications with security, performance, and integration considerations.

When I first started, I made mistakes like hardcoding API keys and ignoring error handling. Now I know better. Skills should be robust, secure, and efficient.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments