Following pi's philosophy: minimal toolset, maximum capability.
| Tool | Purpose |
|---|---|
bash |
Execute shell commands (subsumes grep, glob, find) |
read |
Read file contents (text + images) |
write |
Create or overwrite files |
edit |
Surgical find/replace modifications |
Unless a run uses --unrestricted:
writeandedittargets must remain inside the canonical working directory. Karl resolves symlinks and the nearest existing ancestor, so lexical traversal and symlink escapes are rejected.writeandeditreject.git/**,.karl/**,.env, and.env.*at the workspace root.- a
bashworking-directory override must remain inside the canonical workspace. readis not workspace-scoped; it can read paths outside the working directory.- shell writes require the platform sandbox (Seatbelt on macOS, bubblewrap on Linux). Restricted bash fails closed when that facility is missing or unusable; it never silently runs unsandboxed.
- the sandbox makes the workspace writable while protecting
.git,.karl, root.env, and root.env.*as far as each OS mechanism permits. Seatbelt can deny the names whether or not they exist. Bubblewrap re-mounts existing protected paths read-only, but cannot reserve a protected path that does not exist when the sandbox starts.
--unrestricted is the intentional bypass: it bypasses workspace mutation checks and disables process sandboxing. Use it only when unrestricted host access is intended.
They're just bash commands. The model knows find, grep, rg, fd. Don't duplicate what the shell already does well.
Read handles:
- Binary detection
- Image encoding (for vision models)
- Large file truncation with offset/limit
- Consistent error messages
Extend Karl without forking. Drop a file in ~/.config/karl/tools/:
// ~/.config/karl/tools/jira.ts
import { defineTool } from 'karl-core';
export default defineTool({
name: 'jira',
description: 'Create or update Jira tickets',
parameters: {
type: 'object',
properties: {
action: { enum: ['create', 'update', 'comment'] },
ticket: { type: 'string' },
content: { type: 'string' }
},
required: ['action']
},
async execute({ action, ticket, content }) {
// Your Jira API logic
return { success: true, ticket: 'PROJ-123' };
}
});Tools are loaded at startup. No recompilation needed.