agalwood/Motrix · error · PluginCodedError
plugin.fs.task.not_available_outside_hook
plugin.fs.task.not_available_outside_hook
Error message
fs.task is only available inside a hook invocation; no hook context is active
What it means
Thrown by dispatchFsTask when this.currentFsTaskHost is falsy. The fs.task capability is bound to a single in-flight hook invocation; the host sets currentFsTaskHost via a BridgeHookEnter event (Plan C). Calling fs.task.* outside that window — at module load, onActivate, onStartup, or after the hook returned — throws plugin.fs.task.not_available_outside_hook.
Source
Thrown at src/core/plugin/host/capability-bridge.ts:713
return host.post(
url,
body,
opts as Parameters<HttpCapabilityHost['post']>[2]
)
}
throw new PluginCodedError(
'plugin.capability.unavailable',
`unknown http method: ${msg.method}`
)
}
// -------------------------------------------------------------------------
// fs.task — requires hook context (Plan C). Task 19 throws when no host.
// -------------------------------------------------------------------------
private async dispatchFsTask(msg: BridgeCallMessage): Promise<unknown> {
if (!this.currentFsTaskHost) {
throw new PluginCodedError(
'plugin.fs.task.not_available_outside_hook',
'fs.task is only available inside a hook invocation; no hook context is active'
)
}
const host = this.currentFsTaskHost
switch (msg.method) {
case 'stat':
return host.stat()
case 'exists':
return host.exists()
case 'computeHash': {
const [alg] = msg.args as [Parameters<typeof host.computeHash>[0]]
return host.computeHash(alg)
}
case 'rename': {
const [newName] = msg.args as [string]
return host.rename(newName)
}View on GitHub (pinned to 1a708ee577)
Solutions
- Move all fs.task calls inside a registered beforeCreate/beforeFinalize hook handler where the context is active.
- For persistent plugin-owned files that must be readable outside hooks, use fs.storage instead (it is per-plugin and not hook-scoped).
- If writing tests, drive the bridge through setHookContext(...) (or post a BridgeHookEnter) before invoking fs.task methods.
Example fix
// before — module top-level
const info = await fs.task.stat()
// after — inside a hook
export function beforeFinalize(ctx) {
const info = await ctx.fs.task.stat()
} Defensive patterns
Strategy: type-guard
Validate before calling
// Track whether the plugin is currently inside a hook invocation.
let inHook = false
export function beforeFinalize(ctx) { inHook = true; try { /* ... */ } finally { inHook = false } }
function assertInHook() { if (!inHook) throw new Error('fs.task requires a hook context') } Type guard
function hasFsTaskContext(ctx: unknown): ctx is { fs: { task: object } } {
return !!ctx && typeof (ctx as any)?.fs?.task === 'object'
} Try / catch
try { return await fs.task.stat() }
catch (e) {
if (e instanceof Error && e.code === 'plugin.fs.task.not_available_outside_hook') {
// defer: enqueue the op to run on the next hook invocation
return null
}
throw e
} Prevention
- Never cache fs.task at module scope — re-acquire it inside each hook handler.
- Use fs.storage for plugin-owned persistent files that must be read outside hooks.
- In tests, always drive the bridge through setHookContext before exercising fs.task.
When it happens
Trigger: Plugin calls fs.task.stat()/exists()/computeHash()/rename()/openReader() from its top-level module code, an onActivate handler, or any non-hook code path. The guard at capability-bridge.ts:712-717 fires before the method switch.
Common situations: Plugin initializes state by reading the task file at module load; plugin caches an fs.task reference and calls it later (the host clears currentFsTaskHost when the hook ends); hook context not yet wired in a test harness.
Related errors
- plugin.metadata.not_available_outside_hook
- plugin.lifecycle.secrets_seed_missing
- plugin.lifecycle.deactivate_timeout
- PluginActivationCapExceeded
- plugin.capability.bad_args
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/58b29b82a5bd6c72.
Report an issue: GitHub.