Install
$ agentstack add skill-techymt-claude-code-superpowers-permission-system ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Permission System
The pattern
Every tool call in Claude Code passes through a permission gate before execution. The gate is a checkPermissions(input, context) method defined on the tool itself. The framework calls this method before invoking call() — the tool never calls it manually. If the result has behavior: 'deny', the framework sends a tool_result with is_error: true back to the LLM and never invokes call() at all. This makes the permission boundary declarative: a tool that omits checkPermissions gets the default implementation, which returns { behavior: 'allow' } unconditionally.
Three permission modes control how the gate behaves: 'default' prompts the user for sensitive operations, 'bypass' grants everything silently, and 'auto' approves operations matching pre-approved rules.
Why this matters
Claude Code executes arbitrary shell commands, reads and writes files, and can make network requests. Running in a developer's local environment means a mistake has real consequences — files can be deleted, credentials can be exfiltrated, production systems can be modified. The permission system is a defense-in-depth layer: even if the LLM produces a malicious or incorrect tool call, the user is prompted before the side effect happens.
The system is per-input, not per-tool. checkPermissions receives the full parsed input, so a bash tool can be auto-approved for git status (read-only) while still prompting for rm -rf (destructive). Rules match tool names, command patterns, and file path globs, making the policy composable and auditable.
How to apply it
- Implement
checkPermissions(input, context)as a method on your tool definition — the framework calls it beforecall(), so no manual invocation is needed insidecall(). - Inside
checkPermissions, readcontext.getAppState().toolPermissionContextand pass it to the appropriate utility (e.g.,checkReadPermissionForTool,checkBashPermissionForTool) fromsrc/utils/permissions/. - When defining a new tool, implement
isDestructive?(input)if the tool performs irreversible mutations. This signals to the permission UI that extra confirmation is appropriate. - To add auto-approve rules, write them to
~/.claude/settings.jsonunderpermissions.allow. Rules use glob-like matchers:Bash(git *)approves all git bash commands;FileRead(src/*)approves file reads in src/. - The
toolPermissionContextin AppState carries the current rules — read it viacontext.getAppState().toolPermissionContext.
In the source
// Source: src/Tool.ts (checkPermissions in ToolDef)
// checkPermissions is optional — buildTool provides a default that returns { behavior: 'allow' }
checkPermissions(
input: z.infer,
context: ToolUseContext,
): Promise
// PermissionResult behaviors:
// { behavior: 'allow'; ... } — proceed immediately
// { behavior: 'deny'; message: string; ... } — framework rejects, call() is never invoked
// { behavior: 'ask'; ... } — framework prompts user before proceeding
// { behavior: 'passthrough'; message: string; ... } — delegate to next layer
// Source: src/tools/FileReadTool/FileReadTool.ts (checkPermissions implementation)
async checkPermissions(input, context): Promise {
const appState = context.getAppState()
return checkReadPermissionForTool(
FileReadTool,
input,
appState.toolPermissionContext,
)
},
// call() — framework has already run checkPermissions; no manual permission call needed
async call({ file_path, offset, limit }, context, _canUseTool?) {
const content = await readFile(file_path)
return { data: content }
},
// Source: src/utils/permissions/ (rule matching)
// Permission rules in settings.json:
// {
// "permissions": {
// "allow": ["Bash(git *)", "FileRead(**/*.ts)"],
// "deny": ["Bash(rm *)"]
// }
// }
The key non-obvious detail: Zod schema validation happens BEFORE checkPermissions is called. The framework validates the input first; checkPermissions is only invoked with valid, parsed arguments. This means permission rules can safely inspect typed fields (e.g., input.command) without needing to handle malformed input.
Apply it to your code
Before — tool with no checkPermissions method (executes without any permission check):
{
// No checkPermissions — buildTool default returns { behavior: 'allow' } unconditionally
async call({ file_path }, context, _canUseTool?) {
// Directly executes without asking the user — dangerous for destructive ops
await fs.unlink(file_path)
return { type: 'success', data: `Deleted ${file_path}` }
},
isDestructive: (_input) => true,
isReadOnly: (_input) => false,
}
After — same tool with checkPermissions delegating to the appropriate utility:
{
// checkPermissions is called by the framework before call() — never call it manually
async checkPermissions(input, context): Promise {
const appState = context.getAppState()
return checkWritePermissionForTool(
DeleteFileTool,
input,
appState.toolPermissionContext,
)
},
async call({ file_path }, context, _canUseTool?) {
// Framework already ran checkPermissions — safe to execute here
try {
await fs.unlink(file_path)
return { type: 'success', data: `Deleted ${file_path}` }
} catch (err) {
return { type: 'error', error: formatFileError(err, file_path) }
}
},
isDestructive: (_input) => true,
isReadOnly: (_input) => false,
isConcurrencySafe: (_input) => false, // File deletion is not concurrent-safe
}
Signals that you need this pattern
- A tool performs file writes, shell execution, or network requests without a
checkPermissionsmethod - A tool relies on the default allow-all behavior from
buildToolfor genuinely destructive operations isDestructiveis not implemented for tools that delete, overwrite, or execute system commands- Users report that a tool never prompts for confirmation even in
'default'permission mode - A new tool is added that can access credentials or sensitive paths without any audit trail
Signals that you're over-applying it
- Read-only tools (file reads, glob, grep) don't need
isDestructive—isReadOnly: trueis sufficient signal - If permission mode is
'bypass'(CI/headless),checkPermissionsalways returns allow — don't add extra guards on top - Don't implement custom permission logic inside
call()— usecheckPermissions; custom checks create policy inconsistencies
Works with
tool-definition— wherecheckPermissionsfits in the Tool interfacedomain-model— howtoolPermissionContextlives in AppStateerror-handling— what to return when permission is denied
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: TechyMT
- Source: TechyMT/claude-code-superpowers
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.