Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Meza-dev/Ghostly/llms.txt

Use this file to discover all available pages before exploring further.

Ghostly is built around a single guiding principle: your code, test steps, and credentials never leave your machine unless you explicitly configure them to. The engine runs entirely as a local process on the developer’s host, persists all data in a SQLite file in your home directory, and authenticates every internal request without phoning home to any Ghostly-operated server. This page documents how each layer of that security model works and what you need to do to keep it intact.

Core Security Properties

Local Key Generation

API keys are generated on the developer’s machine by ghostly keygen using Node’s crypto.randomUUID() or randomBytes(32).toString('hex'). No key material is ever generated, stored, or transmitted by a remote server.

No External Data Transmission

Source code, test goals, run artifacts, screenshots, and traces are stored only in ~/.ghostly/ and never sent to Ghostly servers. The only optional outbound traffic is to your configured LLM endpoint.

Auth File Isolation

All credentials live in ~/.ghostly/auth.json. The CLI writes this file with mode: 0o600 (user-read-only). Verify permissions with ls -la ~/.ghostly/auth.json and correct them with chmod 600 ~/.ghostly/auth.json if needed.

Compliance Ready

Because no code or test data leaves the local machine, Ghostly is suitable for environments with strict data-residency, air-gap, or source-code-confidentiality requirements.

API Authentication

The Ghostly API enforces authentication on every route except /v1/auth/. Two authentication methods are accepted, checked in order by the middleware stack:

Outer gate — API Key middleware

The apiKeyMiddleware runs first on all routes. It short-circuits (passes through) under two conditions: the path starts with /v1/auth/, or an Authorization: Bearer header (or ?token= query parameter) is present. For all other requests, it reads the expected key directly from ~/.ghostly/auth.json and compares it to the incoming X-Api-Key header. Requests that provide neither a Bearer token nor a valid API key are rejected with 401 before reaching any route handler.
// apps/api/src/middleware/api-key.ts (simplified)
if (c.req.path.startsWith("/v1/auth/")) return next();
const hasBearer = authHeader?.startsWith("Bearer ");
if (hasBearer || hasQueryToken) return next(); // deferred to authMiddleware

const expected = readExpectedApiKey(); // reads ~/.ghostly/auth.json
const provided = c.req.header("x-api-key")?.trim();
if (!expected || !provided || provided !== expected) {
  return c.json({ ok: false, error: "unauthorized" }, 401);
}

1 — JWT Bearer Tokens

Used by the web dashboard and browser clients. A JWT is issued by POST /v1/auth/login and must be sent in the Authorization: Bearer <token> header (or as a ?token= query parameter for SSE/EventSource connections that cannot set headers). Tokens are HMAC-SHA256 signed using the JWT_SECRET environment variable. The default value is "ghostly-secret" — a placeholder that must be replaced in any deployment beyond your own laptop. Tokens expire after 7 days; the middleware additionally validates that the token’s subject (sub) still exists in the database, guarding against stale sessions after a database reset. Signature verification uses timingSafeEqual to prevent timing side-channel attacks.
// apps/api/src/middleware/auth.ts (simplified)
const secret = process.env.JWT_SECRET ?? "ghostly-secret";
const bearerToken = authHeader?.startsWith("Bearer ")
  ? authHeader.slice(7)
  : queryToken;
if (bearerToken) {
  const payload = verifyToken(bearerToken, secret); // uses timingSafeEqual
  if (!payload) return c.json({ ok: false, error: "unauthorized" }, 401);
  const user = await prisma.user.findUnique({ where: { id: payload.sub } });
  if (!user) return c.json({ ok: false, error: "unauthorized" }, 401);
  c.set("user", user);
  return next();
}
The default JWT_SECRET of "ghostly-secret" is publicly known. Anyone who knows it can forge a valid JWT and gain full API access. Set a strong, random value via the JWT_SECRET environment variable before exposing Ghostly to any network interface other than loopback.

2 — API Key (X-Api-Key Header)

Used by the CLI and the MCP server. The authMiddleware also accepts an X-Api-Key header as a fallback after JWT. It performs a database lookup against the ApiKey table (managed via the dashboard Settings page) and sets the authenticated user context from the matching record. This is distinct from the outer apiKeyMiddleware (which validates against ~/.ghostly/auth.json): the outer gate checks the single installation key at the file-system level, while the inner auth middleware supports multiple per-user API keys stored in the database.
// apps/api/src/middleware/auth.ts (simplified — API key path)
const apiKey = c.req.header("X-Api-Key");
if (apiKey) {
  const record = await prisma.apiKey.findUnique({
    where: { key: apiKey },
    include: { user: { select: { id: true, email: true, role: true } } },
  });
  if (record) {
    c.set("user", record.user);
    return next();
  }
  return c.json({ ok: false, error: "unauthorized" }, 401);
}

Password Hashing

User passwords are hashed with PBKDF2-SHA512 using a random 16-byte salt and 100,000 iterations, producing a 64-byte derived key. The stored format is <hex-salt>:<hex-hash>. Password verification uses timingSafeEqual to prevent timing attacks.
// apps/api/src/lib/password.ts
const ITERATIONS = 100_000;
const KEYLEN = 64;
const DIGEST = "sha512";

export function hashPassword(password: string): string {
  const salt = randomBytes(16).toString("hex");
  const hash = pbkdf2Sync(password, salt, ITERATIONS, KEYLEN, DIGEST).toString("hex");
  return `${salt}:${hash}`;
}

Auth File: ~/.ghostly/auth.json

The auth file is the single source of truth for the local installation. It contains:
  • apiKey — the GHOST_API_KEY value used by CLI and MCP
  • apiUrl — the GHOST_API_URL base URL
  • llm — LLM provider, model, API key, and base URL
  • extraEnv — any additional environment variables to inject into the API process
The CLI writes this file with mode: 0o600 (owner read/write only). If you copy or restore this file, re-apply the permissions manually.
# Verify permissions
ls -la ~/.ghostly/auth.json
# Expected: -rw------- 1 youruser yourgroup ...

# Fix permissions if wrong
chmod 600 ~/.ghostly/auth.json
~/.ghostly/auth.json contains your LLM provider API key in plaintext. Treat it with the same care as an SSH private key. Do not commit it to version control, do not share it, and do not store it in a cloud-synced folder without encryption at rest.

LLM Security

1

Read-only mode for Cursor Agent CLI

When using the cursor-cli provider, Ghostly invokes the agent with --mode ask. This flag restricts the agent to read-only operations — it cannot write files or execute shell commands. Never use --force or --yolo flags with the agent in any Ghostly context.
2

API keys stay local

LLM provider API keys (e.g., OpenAI, Anthropic, OpenRouter) are stored in ~/.ghostly/auth.json and injected into the API process environment at startup. They are never sent to Ghostly servers or logged in run artifacts.
3

Secrets are not included in prompts

Ghostly does not include your API keys, file paths, or other credentials in the natural-language prompts it sends to the LLM. Assisted metadata is redacted before being persisted to the database.
4

Shell injection prevention

When spawning the Cursor Agent CLI, Ghostly uses spawn without shell: true and delivers prompts via stdin (written to a temporary file). This prevents shell injection attacks even if a test goal contains characters that would be interpreted by a shell.
5

Temporary files are cleaned up

Prompt files written to os.tmpdir() are created with mkdtemp and deleted in a finally block after every LLM call, regardless of success or failure.

MCP Security

The MCP server exposes Ghostly tools (ghostly_run_flow, project map, submit plan) to your IDE. Its configuration is written to ~/.cursor/mcp.json by ghostly install.
~/.cursor/mcp.json stores the Ghostly API key in plaintext as an environment variable passed to the MCP server process. Do not commit this file to version control. Add it to your global .gitignore or ensure your project’s .gitignore covers ~/.cursor/.
The MCP server authenticates all outbound requests to the Ghostly API using the X-Api-Key header. It reads the key from the X_API_KEY environment variable, which is set by the mcp.json entry written during ghostly install.

Default Credentials

ghostly up seeds a fresh database with a default admin account:
FieldValue
Emailadmin@ghostly.local
Passwordadmin123
The default admin credentials are hardcoded in the CLI source and are publicly known. Change the admin password immediately after the first login, especially if the server is running on a shared machine or accessible beyond loopback.

Production Hardening Checklist

Authentication & Secrets
  • Change the default admin password (admin@ghostly.local / admin123) via the dashboard Settings page after first login.
  • Set a strong, randomly generated JWT_SECRET environment variable before exposing the API to any network beyond 127.0.0.1. A 32-byte hex string from openssl rand -hex 32 is a safe choice.
  • Rotate the Ghostly API key after any suspected compromise: run ghostly keygen and then ghostly up to restart the server with the new key.
File Permissions
  • Confirm ~/.ghostly/auth.json has permissions 600: chmod 600 ~/.ghostly/auth.json.
  • Confirm ~/.cursor/mcp.json is not tracked in any git repository. Add it to ~/.gitignore_global or your project’s .gitignore.
Network Exposure
  • Keep HOST set to 127.0.0.1 (enforced by ghostly up). If launching the API directly without the CLI, set HOST=127.0.0.1 explicitly — the env var default is 0.0.0.0.
  • If Ghostly must be accessible over a network (e.g., from a VM or Docker host), place it behind a reverse proxy with TLS and restrict access to known IPs.
LLM Provider
  • If using the cursor-cli provider, verify the agent is invoked with --mode ask (Ghostly enforces this, but confirm it in your logs if you have a custom deployment).
  • If using an http LLM provider, prefer endpoints you control (Ollama locally, Azure OpenAI with VNet) over public cloud APIs for maximum data-residency assurance.
  • Never paste LLM provider API keys into test goal strings — they would be sent to the LLM in plaintext.
Compliance & Auditing
  • Review ~/.ghostly/auth.json and ~/.cursor/mcp.json as part of any regular secrets rotation schedule.
  • For air-gapped environments, configure ASSIST_LLM_PROVIDER=cursor-cli with a locally available model, or set ASSIST_ENABLED=false to disable AI assistance entirely.

Threat Model Summary

ThreatMitigation
Forged JWT tokenstimingSafeEqual comparison in verifyToken; strong JWT_SECRET required in production
Stolen API keyInstallation key validated against ~/.ghostly/auth.json at request time; per-user DB keys also supported; rotate with ghostly keygen
Weak password storagePBKDF2-SHA512, 100,000 iterations, random 16-byte salt; timingSafeEqual on verification
LLM prompt injectionPrompts delivered via stdin/temp file with spawn (no shell); --mode ask prevents file writes
Credential exfiltration via LLMSecrets not included in prompts; metadata redacted before DB persistence
Unauthorized network accessghostly up binds to 127.0.0.1; env var default is 0.0.0.0 — set explicitly for direct launches
Default credential abuseSeed credentials are documented and must be rotated post-install

Build docs developers (and LLMs) love