Squid supports a powerful JavaScript-based plugin system that allows you to extend its capabilities with custom tools. Plugins can be invoked by the LLM alongside built-in tools like read_file, write_file, and grep.
Plugins can be stored in three locations:
- Workspace plugins:
./plugins/(project-specific) - Global plugins:
~/.squid/plugins/(shared across projects) - Bundled plugins: Shipped with the executable (when installed from crates)
Workspace plugins override global plugins, which override bundled plugins with the same ID.
Development note: During development (cargo build), the plugins/ directory is automatically copied to target/debug/plugins/ so the executable can find bundled plugins alongside global and workspace plugins.
mkdir -p plugins/my-plugin
cd plugins/my-plugin{
"id": "my-plugin",
"title": "My First Plugin",
"description": "A simple example plugin",
"version": "0.1.0",
"api_version": "1.0",
"security": {
"requires": ["read_file"],
"network": false,
"file_write": false
},
"input_schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Message to process"
}
},
"required": ["message"]
},
"output_schema": {
"type": "object",
"properties": {
"result": {
"type": "string",
"description": "Processed message"
}
},
"required": ["result"]
}
}function execute(context, input) {
try {
context.log(`Processing message: ${input.message}`);
// Your plugin logic here
const result = input.message.toUpperCase();
return { result };
} catch (error) {
return {
error: error.message
};
}
}
globalThis.execute = execute;Edit squid.config.json:
{
"agents": {
"general-assistant": {
"permissions": {
"allow": [
"read_file",
"plugin:my-plugin",
"plugin:*"
]
}
}
}
}Start the server and ask the LLM to use your plugin:
squid serverThen in the web UI: "Please use the my-plugin tool to process the message 'hello world'"
Every plugin consists of two required files:
plugins/
└── my-plugin/
├── plugin.json # Metadata and schemas
└── index.js # Implementation
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | ✓ | Unique identifier (lowercase, hyphens) |
title |
string | ✓ | Human-readable name |
description |
string | ✓ | What the plugin does |
version |
string | ✓ | Semantic version (e.g., "0.1.0") |
api_version |
string | ✓ | Plugin API version (currently "1.0") |
security |
object | ✓ | Security requirements |
input_schema |
object | ✓ | JSON Schema for input validation |
output_schema |
object | ✓ | JSON Schema for output validation |
{
"security": {
"requires": ["read_file", "write_file"],
"network": false,
"file_write": false
}
}requires: Array of built-in tools the plugin needs (e.g.,read_file,grep,bash:ls)network: Whether plugin needs HTTP accessfile_write: Whether plugin needs file write access
Your execute function receives a context object with these APIs:
Read a file from the filesystem (respects .squidignore).
const content = context.readFile("./README.md");
console.log("File size:", content.length);Throws: Error if file doesn't exist or path is not allowed.
Log a message that will be saved to the server's database logs (appears in server logs and can be queried with squid logs).
context.log("Processing started");
context.log("Found 5 issues");Note: console.log() also works and is routed through the same logging system.
Write content to a file (requires file_write: true permission).
const success = context.writeFile("./output.txt", "Hello, World!");
if (success) {
console.log("File written successfully");
}Throws: Error if permission denied or path is not allowed.
Make an HTTP GET request (requires network: true permission).
const html = context.httpGet("https://example.com", 10000);
console.log("Response length:", html.length);Parameters:
url: The URL to fetchtimeout: Optional timeout in milliseconds (default: 5000)
Throws: Error if permission denied, network error, or non-200 status code.
Get the current project directory.
const dir = context.config.projectDir;Your plugin receives structured input and must return structured output that matches your schemas:
function execute(context, input) {
// input is validated against input_schema
const result = processData(input);
// return value is validated against output_schema
return { result };
}Always wrap your logic in try-catch:
function execute(context, input) {
try {
// Your logic
return { success: true, data: result };
} catch (error) {
return {
error: error.message,
success: false
};
}
}Squid uses a hybrid security model:
- Plugins declare what they need (
security.requires) - Agents control what plugins can run (
permissions.allow/deny) - User approves plugin execution (via Web UI)
{
"permissions": {
"allow": [
"plugin:*", // Allow all plugins
"plugin:markdown-linter", // Allow specific plugin
"read_file" // Required by plugins
],
"deny": [
"plugin:dangerous-plugin" // Block specific plugin
]
}
}Plugins run in a sandboxed QuickJS environment:
- ❌ No
eval()orFunction()constructor - ❌ No direct filesystem access (use context APIs)
- ❌ No network access unless
security.network = true - ✓ Memory limit: 128MB (configurable)
- ✓ Timeout: 30 seconds (configurable)
Analyzes markdown files for style issues.
plugin.json:
{
"id": "markdown-linter",
"title": "Markdown Linter",
"description": "Lints markdown files for style issues",
"version": "0.1.0",
"api_version": "1.0",
"security": {
"requires": ["read_file"],
"network": false,
"file_write": false
},
"input_schema": {
"type": "object",
"properties": {
"path": { "type": "string" },
"max_line_length": { "type": "number", "default": 120 }
},
"required": ["path"]
},
"output_schema": {
"type": "object",
"properties": {
"issues": { "type": "array" },
"stats": { "type": "object" }
},
"required": ["issues", "stats"]
}
}index.js:
function execute(context, input) {
const content = context.readFile(input.path);
const maxLen = input.max_line_length || 120;
const issues = [];
const lines = content.split('\n');
lines.forEach((line, i) => {
if (line.length > maxLen) {
issues.push(`Line ${i + 1}: Exceeds max length`);
}
});
return {
issues,
stats: {
lines: lines.length,
headings: (content.match(/^#+\s+/gm) || []).length
}
};
}
globalThis.execute = execute;Formats code snippets.
See plugins/code-formatter/ for complete implementation.
Fetches content from URLs (requires network permission).
See plugins/http-fetcher/ for complete implementation.
Always validate input even though schemas provide basic validation:
function execute(context, input) {
if (!input.path || typeof input.path !== 'string') {
return { error: "Invalid path" };
}
// ...
}Return structured error information:
return {
error: "File not found",
details: {
path: input.path,
suggestion: "Check if the file exists"
}
};Log important operations for debugging and monitoring. Logs are saved to the database:
context.log(`Processing file: ${input.path}`);
context.log(`Found ${issues.length} issues`);
// console.log() also works and goes to the same place
console.log("Debug info:", { count: 42 });View plugin logs with:
squid logs --level info | grep PluginPlugins should complete within the timeout (default: 30s):
// Good: Process in chunks
for (let i = 0; i < items.length; i += 100) {
processChunk(items.slice(i, i + 100));
}
// Bad: Long synchronous operation
for (let i = 0; i < 1000000; i++) {
heavyOperation(i);
}- Plugin ID: lowercase with hyphens (
markdown-linter) - Tool name: automatically prefixed (
plugin:markdown-linter) - Files: exactly
plugin.jsonandindex.js
Check server logs for errors:
squid server --log-level debugCommon issues:
- Invalid JSON in
plugin.json - Missing
executefunction inindex.js - Invalid API version (must be "1.0")
Ensure the agent has permission:
{
"permissions": {
"allow": [
"plugin:your-plugin",
"read_file" // If plugin requires it
]
}
}Check that your input/output matches the schemas:
// Input must match input_schema
return {
issues: [], // Must be array
stats: { // Must be object with required fields
lines: 0,
headings: 0
}
};Increase timeout in config:
{
"plugins": {
"default_timeout_seconds": 60
}
}{
"plugins": {
"enabled": true,
"load_global": true,
"load_workspace": true,
"load_bundled": true,
"default_timeout_seconds": 30,
"max_memory_mb": 128
}
}enabled: Enable/disable plugin systemload_global: Load plugins from~/.squid/plugins/load_workspace: Load plugins from./plugins/load_bundled: Load bundled plugins shipped with the executable (default: true)default_timeout_seconds: Maximum execution timemax_memory_mb: Memory limit per plugin
Plugin configuration can be overridden via environment variables:
SQUID_PLUGINS_LOAD_BUNDLED: Set tofalseto disable loading bundled plugins (e.g.,SQUID_PLUGINS_LOAD_BUNDLED=false)
Important: The current plugin system uses synchronous execution only. JavaScript async/await and Promises are not supported due to limitations in the QuickJS runtime integration (see rquickjs#401).
All plugin execute functions must be synchronous:
// ✓ Correct: Synchronous
function execute(context, input) {
const result = processData(input);
return { result };
}
// ✗ Incorrect: Async not supported
async function execute(context, input) {
const result = await fetchData(); // Will not work!
return { result };
}If your plugin needs async operations (HTTP requests, file I/O), use the context APIs which are implemented in Rust and handle async internally:
function execute(context, input) {
// Use context.httpGet() instead of fetch() - handled by Rust
// Use context.readFile() instead of async file reads
const content = context.readFile(input.path); // Synchronous from JS perspective
return { content };
}Future: Async support may be added if the rquickjs event loop integration improves.
For complex plugins, split logic into modules (future):
// Currently: single index.js file
// Future: support for require() or importsFuture feature: Share plugins via a central marketplace.
- Issues: https://github.com/DenysVuika/squid/issues
- Discussions: https://github.com/DenysVuika/squid/discussions
- Examples: See
plugins/directory for working examples
We welcome plugin contributions! To share your plugin:
- Test it thoroughly
- Document it well
- Submit a PR to add it to the examples
- Consider publishing to the marketplace (coming soon)
Happy plugin building! 🦑🔌