{"record":{"id":"58b29b82a5bd6c72","repo":"agalwood/Motrix","slug":"plugin-fs-task-not-available-outside-hook","errorCode":"plugin.fs.task.not_available_outside_hook","errorMessage":"fs.task is only available inside a hook invocation; no hook context is active","messagePattern":"fs\\.task is only available inside a hook invocation; no hook context is active","errorType":"exception","errorClass":"PluginCodedError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/host/capability-bridge.ts","lineNumber":713,"sourceCode":"      return host.post(\n        url,\n        body,\n        opts as Parameters<HttpCapabilityHost['post']>[2]\n      )\n    }\n    throw new PluginCodedError(\n      'plugin.capability.unavailable',\n      `unknown http method: ${msg.method}`\n    )\n  }\n\n  // -------------------------------------------------------------------------\n  // fs.task — requires hook context (Plan C). Task 19 throws when no host.\n  // -------------------------------------------------------------------------\n\n  private async dispatchFsTask(msg: BridgeCallMessage): Promise<unknown> {\n    if (!this.currentFsTaskHost) {\n      throw new PluginCodedError(\n        'plugin.fs.task.not_available_outside_hook',\n        'fs.task is only available inside a hook invocation; no hook context is active'\n      )\n    }\n    const host = this.currentFsTaskHost\n    switch (msg.method) {\n      case 'stat':\n        return host.stat()\n      case 'exists':\n        return host.exists()\n      case 'computeHash': {\n        const [alg] = msg.args as [Parameters<typeof host.computeHash>[0]]\n        return host.computeHash(alg)\n      }\n      case 'rename': {\n        const [newName] = msg.args as [string]\n        return host.rename(newName)\n      }","sourceCodeStart":695,"sourceCodeEnd":731,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/host/capability-bridge.ts#L695-L731","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — module top-level\nconst info = await fs.task.stat()\n// after — inside a hook\nexport function beforeFinalize(ctx) {\n  const info = await ctx.fs.task.stat()\n}","handlingStrategy":"type-guard","validationCode":"// Track whether the plugin is currently inside a hook invocation.\nlet inHook = false\nexport function beforeFinalize(ctx) { inHook = true; try { /* ... */ } finally { inHook = false } }\nfunction assertInHook() { if (!inHook) throw new Error('fs.task requires a hook context') }","typeGuard":"function hasFsTaskContext(ctx: unknown): ctx is { fs: { task: object } } {\n  return !!ctx && typeof (ctx as any)?.fs?.task === 'object'\n}","tryCatchPattern":"try { return await fs.task.stat() }\ncatch (e) {\n  if (e instanceof Error && e.code === 'plugin.fs.task.not_available_outside_hook') {\n    // defer: enqueue the op to run on the next hook invocation\n    return null\n  }\n  throw e\n}","preventionTips":["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."],"tags":["plugin","capability","fs","hook-context","lifecycle"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}