How to Fix: wasmtime serve seems to ignore –dir option, directory is not preopened

7 min read

Why wasmtime serve --dir looks ignored: your component is running in the wrong execution model

If wasmtime serve --dir ./some-folder still fails with a message like directory is not preopened, the problem usually is not the directory flag itself. The real issue is that WASI HTTP serving and filesystem preopens are wired through different capabilities, and your component must explicitly import and use the right WASI filesystem interfaces for the preopened directory to be visible at runtime.

In other words: --dir does not magically make filesystem access appear inside every wasi:http component. The component must be built against the correct WASI world, and the host must expose the matching capabilities when wasmtime serve instantiates it.

Understanding the Root Cause

This bug is easiest to understand once you separate three layers:

  1. The CLI flag: --dir tells Wasmtime to preopen a host directory for the guest.
  2. The component world: your WebAssembly component must import the relevant WASI filesystem interfaces such as descriptors, directory access, and path operations.
  3. The runner mode: wasmtime serve is specialized for WASI HTTP components, not generic WASI command execution.

When users see directory is not preopened, it typically means one of these is happening:

  • The component was compiled against a world that includes HTTP but does not properly include or consume the expected filesystem interfaces.
  • The code is trying to open a path as though a preopen exists at a certain guest path, but the mapped directory name inside the guest differs from what the program expects.
  • wasmtime serve is instantiating the component in a way where your current component wiring does not expose filesystem access as expected, even though --dir was passed.
  • The project mixes older preview1 assumptions with newer WASI Preview 2 / component model APIs.

This last point is the most common technical mismatch. In preview1, filesystem access often looked like classic file descriptor usage through a different API surface. In Preview 2 and the component model, capabilities are modeled more explicitly through WIT interfaces. If your component is built for wasi:http but your world definition or generated bindings do not actually include wasi:filesystem, then the runtime cannot satisfy filesystem access just because a directory was preopened on the command line.

So the short version is: --dir only provides a host capability; your component still needs the matching guest-side imports and the correct path mapping.

Step-by-Step Solution

The fix is to make your component world, implementation, and runtime invocation line up.

1. Define a world that includes both HTTP and filesystem capabilities

If you are creating a custom WIT world, make sure it imports the WASI HTTP interfaces you need and also the WASI filesystem interfaces your code will use.

package example:server;

world server {
  import wasi:http/incoming-handler;
  import wasi:filesystem/preopens@0.2.0;
  import wasi:filesystem/types@0.2.0;
}

The exact package version and interface names depend on your toolchain, but the important part is this: your world must explicitly include filesystem support. If it only includes HTTP, your handler can serve requests but still fail when attempting to read a file.

2. Generate bindings from the same WIT definitions you actually build with

If you use generated bindings, regenerate them after changing the world file. Stale bindings are a common cause of confusing runtime behavior.

# Example only; use the generator for your language/toolchain
wit-bindgen rust --world server wit/

If your code was generated from an older world definition that lacked filesystem imports, your implementation may compile while still not being able to access the preopened directory correctly at runtime.

3. Access the preopened directory through the correct guest path or preopen descriptor

With capability-based filesystem access, the guest does not automatically see the host filesystem root. It only sees the directories you preopen, usually under a guest-visible mount point.

For example, if you launch Wasmtime like this:

wasmtime serve --dir .::/data app.wasm

then your component should try to read from /data, not from an arbitrary absolute host path like /Users/name/project.

If your Wasmtime version uses a simpler mapping form such as:

wasmtime serve --dir . app.wasm

then your code must discover how that preopen is exposed in the guest, or use the corresponding preopens API to enumerate available directories.

4. Use the filesystem API that matches your WASI generation

If your project is using WASI Preview 2, do not assume classic preview1 file APIs. Instead, use the generated bindings for the imported filesystem interfaces.

Pseudocode flow:

1. Ask the preopens interface for available directories.
2. Find the directory you expect, such as "/data".
3. Open the file relative to that directory descriptor.
4. Read the contents through the Preview 2 filesystem types API.

This is much more reliable than hardcoding a path and assuming it resolves like a native OS path.

5. Run with an explicit mount point

To avoid ambiguity, prefer an explicit guest path mapping when your Wasmtime version supports it.

wasmtime serve --dir ./fixtures::/data target/wasm32-wasip2/debug/app.wasm

Then in your component, read files from the guest path or descriptor rooted at /data.

6. Verify your Wasmtime version

The WASI HTTP and component model support evolved quickly across Wasmtime releases. If you are following examples from a repository or issue thread, make sure your local CLI version matches the expected behavior.

wasmtime --version

If the example project targets a newer set of WIT packages or CLI behavior, upgrade Wasmtime and rebuild the component.

cargo install wasmtime-cli --locked

Or use your package manager’s current release if it provides the required component features.

7. Test with a minimal file read before layering HTTP logic

A practical debugging trick is to reduce the handler to one file read and return the result in the HTTP response. That isolates whether the issue is filesystem capability wiring or unrelated request-handling logic.

// Pseudocode
handle(request) {
  let content = read_file_from_preopen("/data/test.txt");
  return 200 with body content;
}

If this fails with directory is not preopened, the problem is still in your world definition, guest path mapping, or Wasmtime version compatibility.

8. If needed, switch to a world that explicitly composes both interfaces

If your current wasi:http world file was created as a minimal HTTP-only world, expand it into a custom world that intentionally composes both HTTP and filesystem imports. That is often the decisive fix for repositories reproducing this issue.

package demo:fullstack;

world http-with-fs {
  import wasi:http/incoming-handler;
  import wasi:cli/stdout@0.2.0;
  import wasi:filesystem/preopens@0.2.0;
  import wasi:filesystem/types@0.2.0;
}

Then rebuild the component and rerun it with wasmtime serve --dir.

Common Edge Cases

Mixing Preview 1 and Preview 2 APIs

If your code, bindings, or dependencies still expect older WASI preview1 behavior, the component may compile but fail at runtime when run as a modern component under wasmtime serve. Keep the entire stack on the same WASI generation.

Using the wrong guest path

Passing --dir does not mean the guest can read the host path literally. The guest only sees the mounted preopen. Always read from the guest-visible mount point or enumerate preopens programmatically.

Assuming HTTP implies filesystem access

A component can implement wasi:http/incoming-handler perfectly and still have zero filesystem capability. HTTP support and filesystem support are separate imports.

Stale generated bindings

If you changed the WIT world but did not regenerate bindings, your implementation may not actually use the updated interfaces. Delete generated artifacts, regenerate, and rebuild.

Wasmtime CLI version drift

Examples from issue threads, blog posts, or sample repositories can break if the local Wasmtime version uses different interface package versions or slightly different CLI semantics for directory mapping.

Relative paths inside the component

Relative path resolution may not behave the way you expect in a capability-based guest. Prefer opening files relative to a known preopened directory descriptor rather than assuming a process working directory.

FAQ

Why does wasmtime run --dir sometimes work differently from wasmtime serve --dir?

wasmtime run targets a more general execution flow, while wasmtime serve is specialized for WASI HTTP components. If your component world and bindings are centered on HTTP but not filesystem imports, the same directory flag can appear to behave differently because the instantiated capabilities differ.

Do I need a custom WIT world to use both HTTP and filesystem?

In practice, yes, often you do. If the default world you started from only models HTTP, you should compose a custom world that also imports the required wasi:filesystem interfaces so your component can legally request those capabilities.

What does directory is not preopened actually mean?

It means the guest tried to access a path outside the set of directories explicitly granted by the host, or it tried to use filesystem APIs without the expected preopen wiring. The fix is usually to mount the directory explicitly, use the correct guest-visible path, and ensure the component imports the matching filesystem interfaces.

The key takeaway is simple: wasmtime serve --dir is not ignored. The directory is only useful when your WASI HTTP component is built to import and use filesystem capabilities correctly, and when your code reads from the actual guest-visible preopen rather than a host-native path. Once those pieces align, the preopened directory becomes accessible as expected.

Leave a Reply

Your email address will not be published. Required fields are marked *