BlaiseBlaise
Documentation
Runner and PlugsHow-to guides

Redact secrets from messages

Replace patterns in the messages the agent reports before Blaise stores them.

A plug on the way between the agent and Blaise can rewrite what is reported. This one replaces anything that matches a list of patterns.

1. Write the plug

plugs/redact-secrets.ts
import { z } from "zod";
import { definePlug } from "@blaiseagent/runner";

/** Replace secrets in the text the agent reports before Blaise stores it. */
export default definePlug({
  name: "acme/redact-secrets",
  inputs: z.object({
    patterns: z.array(z.string()).min(1),
    replacement: z.string().default("[REDACTED]"),
  }),

  onLoad: ({ inputs: { patterns, replacement } }) => {
    const regexes = patterns.map((source) => new RegExp(source, "g"));
    return {
      redact: (text: string): string =>
        regexes.reduce((result, re) => result.replace(re, replacement), text),
    };
  },

  onMessage(ctx, message, next) {
    if (message.method === "session/update") {
      const { update } = message.params;
      if (
        (update.sessionUpdate === "agent_message_chunk" ||
          update.sessionUpdate === "agent_thought_chunk") &&
        update.content.type === "text"
      ) {
        return next({
          ...message,
          params: {
            ...message.params,
            update: {
              ...update,
              content: {
                ...update.content,
                text: ctx.state.redact(update.content.text),
              },
            },
          },
        });
      }
    }
    return next(message);
  },
});

onLoad builds the regular expressions once per run, from the inputs. onMessage rewrites the text of the agent's message and thought chunks, which is what Blaise stores and shows. It passes every other message on as it is.

2. Configure it

List the plug first. A message that goes up to Blaise passes the plugs from the last to the first, so the first plug redacts what the others added too:

setup:
  - use: ./plugs/redact-secrets.ts
    with:
      patterns:
        - "sk-[A-Za-z0-9]+"
        - "ghp_[A-Za-z0-9]{36}"

3. Test it

import { describe, expect, test } from "vitest";
import type { Message } from "@blaiseagent/runner";
import { createPlugHarness } from "@blaiseagent/runner/testing";
import redactSecrets from "./redact-secrets";

describe("redact-secrets", () => {
  test("replaces secrets in session updates", async () => {
    const update: Message = {
      direction: "from_agent",
      method: "session/update",
      params: {
        sessionId: "s",
        update: {
          sessionUpdate: "agent_message_chunk",
          content: { type: "text", text: "token is sk-abc123" },
        },
      },
    };
    const harness = createPlugHarness(redactSecrets, {
      inputs: { patterns: ["sk-[a-z0-9]+"] },
    });
    const { forwarded } = await harness.message(update);
    expect(JSON.stringify(forwarded)).toContain("token is [REDACTED]");
    expect(JSON.stringify(forwarded)).not.toContain("sk-abc123");
  });
});

Limits

The plug rewrites the text of those chunks and nothing else: tool call details, permission requests and file contents the agent writes are not touched. To cover them, handle those methods too. A secret that the agent splits across two chunks is not matched by either. The agent itself still sees the original text.

On this page