LatentCodeCustomize

Plugins and custom tools

Extend LatentCode with JavaScript or TypeScript: react to events, check or change what the agent does, and give it new tools.

Install a plugin

Plugins published to npm install with one command:

$latentcode plugin some-latentcode-plugin

This installs the package and adds it to the project's config. Add -g to install it for all projects and -f to replace a version that's already installed. You can also list plugins in latentcode.json, with options:

JSONC
{
  "plugin": [
    "some-latentcode-plugin",
    ["another-plugin", { "channel": "#builds" }],
    "./scripts/my-plugin.ts"
  ]
}

Relative paths are resolved from the config file. Start LatentCode with --pure to run without any external plugins, for example to rule one out when something goes wrong. latentcode debug info lists the plugins that are loaded.

Plugins run with your permissions and aren't sandboxed. Only install ones you trust.

Write a plugin

Put a .ts or .js file in .latentcode/plugins/ (project) or ~/.config/latentcode/plugins/ (everywhere). Every export must be a plugin: an async function that receives context and returns the hooks it implements.

.latentcode/plugins/guard.ts
export const Guard = async ({ directory, $ }) => {
  return {
    // Refuse to read secrets, whatever the permission rules say.
    "tool.execute.before": async (input, output) => {
      if (input.tool === "read" && String(output.args.filePath).includes("secrets/")) {
        throw new Error("Reading files in secrets/ is not allowed")
      }
    },

    // Desktop notification when the agent finishes (macOS).
    event: async ({ event }) => {
      if (event.type === "session.idle") {
        await $`osascript -e 'display notification "LatentCode finished" with title "${directory}"'`
      }
    },
  }
}

The plugin package, with types for plugins and tools, is installed into each .latentcode folder automatically. The context includes directory and worktree (the project), project, and $ for running shell commands.

Hooks

HookCalledYou can
eventFor every event: sessions starting and going idle, messages, permission requests, and more.React, for example notify or log.
tool.execute.beforeBefore a tool runs, with its arguments.Change the arguments, or throw to block the call.
tool.execute.afterAfter a tool runs.Change the title, output or metadata the agent sees.
permission.askWhen a permission rule would ask.Set status to allow, deny or ask.
shell.envBefore a shell command runs.Add environment variables.
chat.messageWhen you send a message.Change the message and its parts.
chat.paramsBefore each model request.Change temperature and provider options.
chat.headersBefore each model request.Add HTTP headers.
command.execute.beforeBefore a slash command runs.Change the parts it sends.
configAt startup, with the merged config.Read the configuration.
toolAt startup.Add tools (below).

Custom tools

To give the agent a new tool without writing a full plugin, put a file in .latentcode/tools/ or ~/.config/latentcode/tools/:

.latentcode/tools/ticket.ts
import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Look up a ticket in the tracker by its key, e.g. PAY-123",
  args: {
    key: tool.schema.string().describe("Ticket key"),
  },
  async execute(args, context) {
    const res = await fetch(`https://tracker.example.com/api/tickets/${args.key}`, {
      headers: { Authorization: `Bearer ${process.env.TRACKER_TOKEN}` },
    })
    return await res.text()
  },
})
  • A default export is named after the file (ticket). Named exports are called <file>_<export>, so one file can hold several tools.
  • tool.schema is Zod; describe each argument so the model knows what to pass.
  • Return a string, or { output, title, metadata }. Long output is truncated like any other tool's.
  • context has the sessionID, agent, directory, worktree, an abort signal, and ask() to request permission.
  • The tool can call anything: shell out to a Python or Go script, or call an API as above.
  • Plugins can add tools the same way, under a tool key in the hooks they return.

Permissions for custom tools

A custom tool runs without asking unless it calls context.ask() itself. To keep an agent from using it, deny it by name: "permission": { "ticket": "deny" }, globally or under an agent.