Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remorses/kimaki/llms.txt
Use this file to discover all available pages before exploring further.
Permission System
Kimaki integrates with OpenCode’s permission system to control which operations the AI can perform on your machine.
Permission Levels
OpenCode supports three permission levels:
allow: Run automatically without asking
ask: Show permission prompt in Discord (3 buttons: Accept, Accept Always, Deny)
deny: Block the operation
Default Permissions
By default, most tools run without asking. The main exception is external_directory - any tool that accesses paths outside the project directory will prompt for approval.
// Built-in default behavior
permission: {
external_directory: {
'*': 'ask' // Prompt for any external path access
}
}
Configuration
Configure permissions in your project’s opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"*": "ask",
"git *": "allow",
"npm *": "allow",
"rm *": "deny"
},
"external_directory": {
"~/other-project/**": "allow"
}
}
}
Pattern Matching
Permission patterns support wildcards:
*: Matches any sequence of characters
?: Matches a single character
**: Matches directories recursively (for paths)
{
"permission": {
"bash": {
"git *": "allow", // Allow all git commands
"npm install *": "allow", // Allow npm install
"rm -rf *": "deny" // Block destructive rm
},
"external_directory": {
"~/.config/**": "deny", // Block config access
"/tmp/**": "allow" // Allow tmp access
}
}
}
Per-Agent Permissions
If using multiple agents, configure per-agent permissions:
{
"agents": [
{
"name": "restricted",
"permission": {
"bash": {
"*": "ask",
"git status": "allow"
}
}
}
]
}
Permission Flow
Request from OpenCode
When the AI attempts a restricted operation:
// 1. OpenCode emits permission.request event
event = {
type: 'permission.request',
data: {
id: 'perm_abc123',
permission: 'bash',
patterns: ['rm -rf node_modules'],
directory: '/path/to/project'
}
}
// 2. Kimaki receives event in session-handler.ts
Display in Discord
Kimaki shows permission buttons:
await showPermissionButtons({
thread,
permission,
directory,
permissionDirectory
})
// Creates message:
// ⚠️ Permission Required
// Type: `bash`
// Pattern: `rm -rf node_modules`
// [Accept] [Accept Always] [Deny]
Buttons:
- Accept: Approve this one request
- Accept Always: Auto-approve similar requests for the rest of the session
- Deny: Block the request
User Response
// User clicks button
async function handlePermissionButton(interaction: ButtonInteraction) {
const response = 'once' | 'always' | 'reject'
// Send reply to OpenCode
await client.permission.reply({
requestID: permission.id,
directory: permissionDirectory,
reply: response
})
// Update message
await interaction.editReply({
content: '✅ Permission accepted',
components: [] // Remove buttons
})
}
OpenCode Processes Reply
OpenCode receives the reply and:
- Accept: Runs the operation
- Accept Always: Runs the operation and auto-approves similar patterns for this session
- Deny: Cancels the operation and informs the AI
Permission Deduplication
Multiple identical permission requests are batched:
function buildPermissionDedupeKey({
permission,
directory
}) {
// Sort patterns for consistent comparison
const sorted = [...permission.patterns].sort()
return `${directory}::${permission.permission}::${sorted.join('|')}`
}
// If same key arrives while pending:
context.requestIds.push(permission.id)
// User's button click answers all batched requests
Example: If the AI tries to run git status three times while waiting for approval, only one button prompt appears. Clicking “Accept” approves all three.
Pattern Coverage Detection
Kimaki detects when new permission requests are covered by previous ones:
export function arePatternsCoveredBy({
patterns,
coveringPatterns
}) {
return patterns.every(pattern =>
coveringPatterns.some(coveringPattern =>
wildcardMatch({ value: pattern, pattern: coveringPattern })
)
)
}
// Example:
arePatternsCoveredBy({
patterns: ['git status', 'git log'],
coveringPatterns: ['git *']
}) // true - both covered by 'git *'
If a pending permission already covers the new request, the new one is added to the existing context without showing a new button.
Auto-Rejection
Pending permissions are auto-rejected when:
- New message arrives: User sends new message before answering
- Session aborted: User runs
/abort
// Auto-reject ALL pending permissions for this thread
const threadPermissions = pendingPermissions.get(threadId)
for (const [permId, pendingPerm] of threadPermissions) {
// Remove buttons
await message.edit({ components: [] })
// Reject via API
await client.permission.reply({
requestID: permId,
directory: pendingPerm.permissionDirectory,
reply: 'reject'
})
}
pendingPermissions.delete(threadId)
Wildcard Matching
Pattern matching implementation:
function wildcardMatch({ value, pattern }) {
// Escape regex special chars
let escapedPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*') // * → .*
.replace(/\?/g, '.') // ? → .
// Special case: "git *" matches "git" alone
if (escapedPattern.endsWith(' .*')) {
escapedPattern = escapedPattern.slice(0, -3) + '( .*)?'
}
return new RegExp(`^${escapedPattern}$`, 's').test(value)
}
// Examples:
wildcardMatch({ value: 'git status', pattern: 'git *' }) // true
wildcardMatch({ value: 'git', pattern: 'git *' }) // true
wildcardMatch({ value: 'npm install', pattern: 'npm *' }) // true
wildcardMatch({ value: 'rm -rf /', pattern: 'rm *' }) // true
Pattern Compaction
Display patterns are compacted to avoid redundancy:
function compactPermissionPatterns(patterns: string[]) {
const unique = Array.from(new Set(patterns))
// Remove patterns covered by other patterns
return unique.filter((pattern, index) =>
!unique.some((candidate, candidateIndex) => {
if (candidateIndex === index) return false
return wildcardMatch({ value: pattern, pattern: candidate })
})
)
}
// Example:
compactPermissionPatterns([
'git status',
'git log',
'git *'
])
// Returns: ['git *']
// (specific patterns removed as they're covered by 'git *')
Common Permission Patterns
Safe by Default
{
"permission": {
"bash": {
"*": "ask",
"git status": "allow",
"git diff *": "allow",
"git log *": "allow",
"ls *": "allow",
"cat *": "allow"
}
}
}
Development Environment
{
"permission": {
"bash": {
"*": "ask",
"git *": "allow",
"npm *": "allow",
"pnpm *": "allow",
"bunx *": "allow",
"curl *": "allow"
},
"external_directory": {
"~/.config/**": "deny",
"~/Documents/**": "deny"
}
}
}
Strict Security
{
"permission": {
"bash": {
"*": "deny",
"git status": "allow",
"git diff": "allow",
"ls": "allow"
},
"edit": {
"*": "ask"
},
"external_directory": {
"*": "deny"
}
}
}
Reloading Configuration
Permission changes in opencode.json require OpenCode server restart:
// Discord command
/restart-opencode-server
// Or restart Kimaki bot
kill -SIGUSR2 <PID>
The server reads opencode.json on startup. Changes during runtime are not detected.
Debugging Permissions
If permissions aren’t working as expected:
-
Check
opencode.json syntax:
# Validate JSON
jq . opencode.json
-
Check OpenCode logs:
# Kimaki log file
tail -f ~/.kimaki/kimaki.log | grep PERMISSION
-
Test pattern matching:
// In Node.js REPL
function wildcardMatch({ value, pattern }) {
let escapedPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.')
if (escapedPattern.endsWith(' .*')) {
escapedPattern = escapedPattern.slice(0, -3) + '( .*)?'
}
return new RegExp(`^${escapedPattern}$`, 's').test(value)
}
wildcardMatch({ value: 'npm install express', pattern: 'npm *' })
-
Verify server restart:
# Check if server is using new config
curl http://127.0.0.1:<port>/api/health
Permission Context Storage
Pending permissions are stored in memory:
export const pendingPermissions = new Map<
string, // threadId
Map<
string, // permissionId
{
permission: PermissionRequest
messageId: string
directory: string
permissionDirectory: string
contextHash: string
dedupeKey: string
}
>
>()
They are not persisted to SQLite. If the bot restarts, pending permissions are lost and must be re-requested.
Best Practices
Start Restrictive
Begin with ask for all operations:
{
"permission": {
"bash": { "*": "ask" },
"edit": { "*": "ask" },
"external_directory": { "*": "ask" }
}
}
Then gradually add allow patterns based on usage.
Use Specific Patterns
Prefer specific patterns over broad wildcards:
// ✅ Good
{
"bash": {
"git status": "allow",
"git diff *": "allow",
"npm ci": "allow"
}
}
// ❌ Risky
{
"bash": {
"*": "allow" // Allows everything!
}
}
Deny Dangerous Commands
{
"bash": {
"rm -rf /*": "deny",
"rm -rf ~/*": "deny",
"chmod -R *": "deny",
"chown -R *": "deny"
}
}
External Directory Allowlist
Explicitly allow known safe paths:
{
"external_directory": {
"*": "deny",
"~/projects/**": "allow",
"/tmp/**": "allow"
}
}
Use “Accept Always” Carefully
Clicking “Accept Always” auto-approves similar patterns for the entire session. Use it only for operations you trust completely (e.g., git status, npm install).
The “always” rule is lost when:
- Session ends
- Bot restarts
- OpenCode server restarts
For complete permission configuration options, see the OpenCode Permissions documentation.