Install
$ agentstack add skill-techymt-claude-code-superpowers-build-tool-factory ✓ 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 No
- ✓ 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
buildTool() Factory
The pattern
Factory functions for capability objects solve a common problem: a typed interface has many methods, but any given implementation only needs to override a few. Listing all methods explicitly creates noise and makes the "what does this tool actually do" harder to see. A factory fills in safe defaults and lets the author focus on the non-trivial fields.
The pattern has three parts: (1) a minimal definition type (ToolDef) where defaultable methods are optional, (2) a factory (buildTool) that merges your definition with safe defaults, and (3) lazy schema getters so schemas are built on first access, not at module load time.
// DefaultableToolKeys — methods buildTool fills in with safe defaults
type DefaultableToolKeys =
| 'isEnabled'
| 'isConcurrencySafe'
| 'isReadOnly'
| 'isDestructive'
| 'checkPermissions'
| 'toAutoClassifierInput'
| 'userFacingName'
// ToolDef — what you pass to buildTool (same as Tool but defaultable methods are optional)
export type ToolDef = Omit, DefaultableToolKeys> &
Partial, DefaultableToolKeys>>
// buildTool fills in safe defaults for all DefaultableToolKeys
export function buildTool(def: D): BuiltTool
| Default key | Safe default value | |---|---| | isConcurrencySafe | () => false | | isReadOnly | () => false | | isDestructive | () => false | | checkPermissions | { behavior: 'allow' } | | isEnabled | () => true | | userFacingName | derived from name |
Why this matters
Claude Code has ~30 tools. Each shares the same set of safety defaults: assume not concurrent-safe, assume not read-only, assume not destructive, assume checkPermissions passes through. Without a factory, every new tool must explicitly declare all of these even when the intent is "no override". The factory encodes the security-safe posture as the default: new tools are restrictive until explicitly opted out.
The alternative — export const X: Tool = { ... } — forces the author to repeat every defaultable method for each tool. It also makes new tools opt-in to safety (you must explicitly set isDestructive: () => false) rather than opt-out (the factory sets it for you and you override only when needed).
How to apply it
- Create
src/tools/[Name]Tool/[Name]Tool.ts(or.tsxif the tool renders JSX output) - Define
inputSchemawithlazySchema(() => z.strictObject({...}))— usez.strictObjectto reject unknown keys - Define
outputSchemathe same way when the tool returns structured data the LLM needs to inspect - Call
buildTool({ name, inputSchema getter, call, ... })— only override methods that differ from the defaults - Override
isConcurrencySafeandisReadOnlyif your tool is side-effect-free (defaults arefalse) - Implement
checkPermissionsif your tool accesses files, shell, or network - Register the tool in
src/tools.ts
In the source
// Source: src/tools/FileReadTool/FileReadTool.ts
import { buildTool, type ToolDef } from '../../Tool.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { z } from 'zod/v4'
const inputSchema = lazySchema(() =>
z.strictObject({
file_path: z.string().describe('Absolute path to the file'),
limit: z.number().int().positive().optional().describe('Max lines'),
})
)
type InputSchema = ReturnType
export type Input = z.infer
export const FileReadTool = buildTool({
name: FILE_READ_TOOL_NAME,
searchHint: 'read files, images, PDFs, notebooks', // helps ToolSearch find deferred tools
strict: true, // rejects unknown input fields
async description() { return 'Read a file' },
// Lazy getter — inputSchema() is not called at module load time
get inputSchema(): InputSchema { return inputSchema() },
// Overrides the false default — this tool is safe to run concurrently
isConcurrencySafe() { return true },
// Overrides the false default — this tool makes no mutations
isReadOnly() { return true },
// checkPermissions is called by the framework BEFORE call() — not inside call()
async checkPermissions(input, context): Promise {
return checkReadPermissionForTool(FileReadTool, input, context.getAppState().toolPermissionContext)
},
async call({ file_path, limit }, context, _canUseTool?) {
const content = await readFileInRange(file_path, 1, limit)
return { data: { type: 'text' as const, file: { filePath: file_path, content } } }
},
})
Notice isConcurrencySafe and isReadOnly are the only defaultable methods overridden here — everything else (isDestructive, isEnabled, userFacingName, toAutoClassifierInput) is left to buildTool's safe defaults.
Apply it to your code
Before — a new capability added as a plain function (no schema, no factory, no lazy schema):
// No schema, no permission check, no lazy loading
async function summariseAsset(assetId: string, depth: number): Promise {
const asset = await fetchAsset(assetId)
return buildSummary(asset, depth)
}
After — same capability as a proper buildTool({...}) with lazySchema, checkPermissions, and { data: result } return:
import { buildTool } from '../../Tool.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { z } from 'zod/v4'
const inputSchema = lazySchema(() =>
z.strictObject({
asset_id: z.string().describe('Qualified name or GUID of the asset'),
depth: z.number().int().min(1).max(3).optional().default(1)
.describe('How many relationship hops to include'),
})
)
type InputSchema = ReturnType
export const SummariseAssetTool = buildTool({
name: SUMMARISE_ASSET_TOOL_NAME,
async description() { return 'Summarise an Atlan asset and its relationships' },
get inputSchema(): InputSchema { return inputSchema() },
// Read-only network call — safe to run concurrently
isConcurrencySafe: () => true,
isReadOnly: () => true,
async checkPermissions(input, context) {
return checkNetworkPermission(SummariseAssetTool, input, context)
},
async call({ asset_id, depth }, context) {
const asset = await fetchAsset(asset_id, context.getAppState().atlanClient)
const summary = buildSummary(asset, depth)
return { data: { summary, assetId: asset_id } }
},
})
Signals that you need this pattern
- A new tool is defined as
export const X: Tool = { ... }(direct interface implementation — bypasses the factory and forces explicit defaults) - A new tool's
inputSchemais a barez.object({...})at module scope instead of wrapped inlazySchema()— schema is built at import time, hurting startup - A new tool implements all 7 defaultable methods explicitly when most are just the default value
- A new tool uses
z.objectinstead ofz.strictObject— unknown input fields will silently pass through validation
Signals that you're over-applying it
- Don't use
buildToolfor test helpers or mock tools — it's for production tools registered intools.ts - Don't add
outputSchemafor tools that return plain strings or simple unstructured data searchHintis only needed for large tools likely to be deferred by ToolSearch; small tools don't need it
Works with
tool-definition— the full Tool interface and what each method doespermission-system— implementingcheckPermissionsfor tools with side effectshot-paths— whylazySchema()matters for startup performancemodule-organisation— where to put the new tool file and how to register it
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.