How portable-files Enables Workspace Portability Across Machines in OpenWork

The portable-files module in OpenWork enables workspace portability by whitelisting specific .opencode subdirectories, validating file paths to block secrets and traversal attacks, and providing atomic export and import functions that serialize user-defined agents, plugins, and tools for safe transfer between machines.

OpenWork stores reusable workspace components—agents, plugins, and tools—inside a hidden .opencode directory. According to the different-ai/openwork source code, the portable-files module (located in apps/server/src/portable-files.ts) implements a strict validation and serialization layer that makes these resources truly portable while preventing the accidental inclusion of sensitive environment files or non-portable dependencies.

Whitelisted Resource Architecture

The portability system relies on an explicit whitelist defined by the ALLOWED_PORTABLE_PREFIXES constant. Only files residing under .opencode/agents/, .opencode/plugins/, or .opencode/tools/ are considered candidates for export. This architectural decision ensures that machine-specific configurations, dependency folders, and secret files remain local to the original workspace.

When listPortableFiles(workspaceRoot) executes, it traverses the .opencode directory and returns an ordered array of objects containing the relative path and string content for each whitelisted file. This explicit enumeration prevents implicit file inclusion and guarantees that the receiving machine reconstructs an identical resource structure. Complementary logic in apps/server/src/portable-opencode.ts handles sanitization of the top-level OpenCode configuration that accompanies these resources.

Path Validation and Security Controls

Before any file enters the portable pipeline, normalizePortablePath processes the input to eliminate Windows-style separators, collapse duplicate slashes, and reject empty or malicious paths. The function specifically blocks directory traversal attempts (such as ../outside.md) and filters reserved segments defined in RESERVED_PORTABLE_SEGMENTS, notably excluding node_modules directories.

Additional security layers include the isEnvFilePath check, which prevents .env files from being serialized, and strict validation within planPortableFiles(workspaceRoot, value). This planning function converts raw portable data into PlannedPortableFile objects with absolute destination paths, throwing an ApiError (defined in apps/server/src/errors.js) immediately if any entry violates the whitelist or path safety rules.

The Export and Import Workflow

Exporting a workspace's portable resources requires calling listPortableFiles, which produces a serializable array of file objects suitable for version control or network transmission. On the destination machine, writePortableFiles(workspaceRoot, value, {replace}) handles reconstruction.

The write operation first validates the input through planPortableFiles, then optionally removes existing portable files when the replace flag is set to true. It utilizes ensureDir (from apps/server/src/utils.js) to create necessary directory structures before writing content, returning a list of successfully written files to confirm the operation. This idempotent approach ensures that repeated imports produce consistent results without duplicating artifacts.

Implementation Examples

The following patterns demonstrate common portability tasks using the OpenWork API.

Export portable files from a workspace:

import { listPortableFiles } from "./portable-files.js";

const workspaceRoot = "/path/to/my/workspace";
const portable = await listPortableFiles(workspaceRoot);
console.log(portable);
/* Example output:
[
  { path: ".opencode/agents/openwork.md", content: "# agent\n" },

  { path: ".opencode/plugins/router.json", content: '{"enabled":true}\n' },
  { path: ".opencode/tools/database.ts", content: "export default {};\n" },
]
*/

Import portable files into a new workspace:

import { writePortableFiles } from "./portable-files.js";

const newWorkspace = "/path/to/new/workspace";
const portableData = [
  { path: ".opencode/agents/openwork.md", content: "# agent\n" },

  { path: ".opencode/plugins/router.json", content: '{"enabled":true}\n' },
  { path: ".opencode/tools/database.ts", content: "export default {};\n" },
];

// Write and replace any existing portable files
await writePortableFiles(newWorkspace, portableData, { replace: true });

Validate a portable file list before writing:

import { planPortableFiles } from "./portable-files.js";

try {
  const planned = planPortableFiles(workspaceRoot, [
    { path: ".opencode/agents/bad/../evil.md", content: "oops" },
  ]);
  // Won't reach here – an error is thrown for invalid paths
} catch (err) {
  console.error(err.message); // "invalid_portable_file_path"
}

Summary

  • The portable-files module strictly limits portability to three whitelisted directories within .opencode: agents/, plugins/, and tools/.
  • Path normalization via normalizePortablePath prevents directory traversal attacks and normalizes cross-platform path separators.
  • Security filters block .env files and node_modules directories from serialization, protecting secrets and reducing payload size.
  • Export operations use listPortableFiles to generate serializable {path, content} arrays, while import operations use writePortableFiles with an optional replace flag for idempotent reconstruction.
  • The planPortableFiles function provides pre-validation, throwing ApiError exceptions before any filesystem mutations occur.

Frequently Asked Questions

What files does OpenWork consider portable?

OpenWork considers only files within .opencode/agents/, .opencode/plugins/, and .opencode/tools/ as portable resources. The ALLOWED_PORTABLE_PREFIXES constant in apps/server/src/portable-files.ts enforces this whitelist, ensuring that only user-defined configurations and scripts—not dependencies or environment files—transfer between machines.

How does portable-files prevent security vulnerabilities?

The module implements multiple validation layers: normalizePortablePath rejects traversal sequences like ../ and normalizes separators, isEnvFilePath blocks environment files containing secrets, and RESERVED_PORTABLE_SEGMENTS excludes node_modules. Additionally, planPortableFiles validates all paths before writePortableFiles touches the filesystem, throwing ApiError for any violation.

Can I overwrite existing portable files when importing?

Yes. The writePortableFiles function accepts an optional {replace: true} parameter. When enabled, the function first removes existing portable files in the target workspace before writing the new set, ensuring a clean state. Without this flag, the function may fail or leave stale files depending on the specific implementation details in apps/server/src/portable-files.ts.

What is the difference between planPortableFiles and writePortableFiles?

planPortableFiles performs validation and path resolution without modifying the filesystem, returning a structured PlannedPortableFile array or throwing ApiError if validation fails. writePortableFiles calls the planning function internally, then executes the actual directory creation (via ensureDir) and file writes. Use planPortableFiles for pre-flight checks or custom processing, and writePortableFiles for standard import operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →