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

  1. Move all fs.task calls inside a registered beforeCreate/beforeFinalize hook handler where the context is active.
  2. For persistent plugin-owned files that must be readable outside hooks, use fs.storage instead (it is per-plugin and not hook-scoped).
  3. 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

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


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/58b29b82a5bd6c72. Report an issue: GitHub.