Security Guidelines

Every Ghosty plugin runs inside users' personal AI assistants. This privileged position demands a high security bar. This page explains the AI-powered review process, common rejection reasons, and best practices for building secure plugins.

The review process

When you upload your plugin via the Submission Portal, it enters a multi-stage review pipeline:

Stage 1: Automated static analysis

An AI model scans your source code for known vulnerability patterns:

  • Injection risks -- innerHTML with unsanitised user input, eval usage, dynamic script injection.
  • Data exfiltration -- network requests to undeclared domains, base64-encoded URLs, obfuscated code.
  • Permission abuse -- SDK calls that require permissions not declared in the manifest.
  • Dependency audit -- known CVEs in bundled third-party libraries.
  • Manifest validation -- schema compliance, permission consistency, size constraints.

Stage 2: Dynamic sandbox testing

The plugin is loaded in an instrumented sandbox environment and exercised automatically:

  • All lifecycle hooks are invoked and monitored for errors, excessive memory usage, and timeout violations.
  • Network requests are intercepted and verified against the allowed_domains list.
  • Storage operations are monitored for quota compliance.
  • The UI is rendered and screenshot-compared against accessibility guidelines.

Stage 3: Human review (conditional)

Plugins that request high-risk permissions (network.fetch, network.websocket, notifications.system) or that trigger warnings in stages 1-2 are escalated to a human reviewer. Human review typically completes within 48 hours.

Review timeline

Most plugins that pass automated review are published within 2 hours. Plugins escalated to human review take up to 48 hours. You will receive email notifications at each stage.

Common rejection reasons

The following issues are the most frequent causes of rejection. Address them before submitting to avoid delays.

1. Unsanitised HTML injection

Rejected: Inserting user-controlled data directly into innerHTML without escaping.

// BAD -- XSS vulnerability
container.innerHTML = `<h1>${userInput}</h1>`;
 
// GOOD -- escaped
container.innerHTML = `<h1>${escapeHtml(userInput)}</h1>`;
 
// BEST -- use textContent for plain text
const h1 = document.createElement("h1");
h1.textContent = userInput;
container.appendChild(h1);

2. Excessive permissions

Rejected: Requesting permissions the plugin does not use.

# BAD -- requests network.fetch but never makes HTTP requests
permissions:
  - storage.read
  - storage.write
  - network.fetch     # unused!
  - notifications.system  # unused!

Only request permissions that your code actually calls. The static analyser cross-references your manifest permissions against SDK usage in your source.

3. Undeclared network domains

Rejected: Making fetch requests to domains not listed in allowed_domains.

# BAD -- fetches from api.example.com but doesn't declare it
permissions:
  - network.fetch
allowed_domains:
  - api.weather.com
// This will be flagged because api.example.com is not declared
await fetch("https://api.example.com/data");

4. Use of eval or new Function

Rejected: Dynamic code execution is blocked by default.

// BAD -- will be rejected
const result = eval(userCode);
const fn = new Function("return " + expr);

If you absolutely need dynamic evaluation (e.g. a calculator plugin), set sandbox.allow_eval: true in the manifest and provide a written justification in your submission notes. These submissions always go to human review.

5. Missing error handling

Rejected: Lifecycle hooks that throw unhandled errors or leave the plugin in a broken state.

// BAD -- unhandled rejection crashes the plugin
onInit: async () => {
  const data = await fetch("https://api.example.com/data");
  // If this fails, the plugin shows nothing
  renderWidget(await data.json());
},
 
// GOOD -- fallback on error
onInit: async () => {
  try {
    const data = await fetch("https://api.example.com/data");
    renderWidget(await data.json());
  } catch {
    renderErrorState("Unable to load data. Tap to retry.");
  }
},

6. Obfuscated or minified source

Rejected: Source code that is intentionally obfuscated to hide its behaviour. Standard minification (webpack, esbuild, terser) is fine, but obfuscation tools (javascript-obfuscator, jsfuck) are not allowed.

Source maps required

If you submit minified code, you must include source maps. The review pipeline uses source maps to analyse the original source. Submissions without source maps are automatically rejected.

High-risk permissions

These permissions receive extra scrutiny during review:

High-risk permissions requiring additional justification

PermissionDescriptionRisk
network.fetchOutbound HTTP requests. Must declare all target domains.high
network.websocketPersistent WebSocket connections. Must declare all target domains.high
notifications.systemOS-level system notifications that appear outside the app.high
user.idAccess to the user's anonymous unique identifier.medium

Best practices

Input validation

Validate and sanitise all external input, whether from user interactions, storage reads, or API responses.

function sanitiseInput(raw: unknown): string {
  if (typeof raw !== "string") return "";
  // Strip control characters and limit length
  return raw.replace(/[\x00-\x1f]/g, "").slice(0, 500);
}

Content Security Policy

The default CSP for plugins is default-src 'self'. If you need to load external resources (fonts, images, API calls), declare them in the manifest:

sandbox:
  csp: "default-src 'self'; img-src 'self' https://images.example.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com"
Never use unsafe-inline

Do not add unsafe-inline or unsafe-eval to your CSP. Both will be flagged during review and almost certainly rejected.

Secure storage practices

  • Never store sensitive credentials (API keys, tokens) in plain text in plugin storage. If you must store secrets, use the ghosty.storage.setSecure API which encrypts values at rest.
  • Always validate data read from storage. Treat storage as untrusted input because users can manually edit storage values through the Ghosty developer tools.
  • Set reasonable TTLs on cached data and refresh when stale.
interface CachedData<T> {
  value: T;
  expiresAt: number;
}
 
async function getCached<T>(key: string, ttlMs: number, fetcher: () => Promise<T>): Promise<T> {
  const cached = await ghosty.storage.get(key) as CachedData<T> | null;
 
  if (cached && cached.expiresAt > Date.now()) {
    return cached.value;
  }
 
  const fresh = await fetcher();
  await ghosty.storage.set(key, {
    value: fresh,
    expiresAt: Date.now() + ttlMs,
  });
 
  return fresh;
}

Dependency management

  • Pin all dependency versions in package.json. Do not use ^ or ~ ranges.
  • Audit dependencies before submission: npm audit --production.
  • Minimise your dependency tree. Fewer dependencies mean less attack surface.
  • Avoid abandoned packages (no updates in over 12 months).

Error reporting

Return generic error messages to users. Log detailed information for your own debugging:

try {
  const result = await riskyOperation();
  return result;
} catch (error) {
  // Generic message for the user
  ghosty.notifications.toast("Something went wrong. Please try again.", {
    variant: "error",
  });
 
  // Detailed log for the developer dashboard
  console.error("[my-plugin] riskyOperation failed:", {
    message: error instanceof Error ? error.message : "Unknown error",
    timestamp: new Date().toISOString(),
  });
 
  return null;
}

Security checklist

Before submitting your plugin, verify each of the following:

  • All user input is escaped before insertion into the DOM.
  • innerHTML is never used with raw user input.
  • Only necessary permissions are requested in the manifest.
  • All network domains are listed in allowed_domains.
  • No eval(), new Function(), or document.write() usage.
  • All lifecycle hooks have error handling with user-friendly fallbacks.
  • Dependencies are pinned and audited for known vulnerabilities.
  • Source maps are included if the bundle is minified.
  • Storage values are validated on read (treated as untrusted input).
  • No secrets, API keys, or credentials are hard-coded in source files.
  • The CSP does not include unsafe-inline or unsafe-eval.
  • Error messages shown to users do not leak internal details.