Skip to main content
Glama

https://github.com/user-attachments/assets/ed36654a-29c4-46ee-b331-5b9afde2b932

waymark

Human oversight as a property of the system, not a property of the agent.

Waymark is MCP middleware that sits between an AI agent and the filesystem. Every write_file and bash call is evaluated against your policy before it touches anything — blocked, held for approval, or logged and allowed. Nothing the agent does is permanent until you say so.

npm downloads npm downloads npm version docs

npm download chart

Updated every 6 hours via GitHub Actions


Three Pillars

Control — enforce the rules you set, unconditionally

Policy lives in waymark.config.json, not in a system prompt. An agent cannot forget a rule, be jailbroken past it, or accumulate enough context to override it. Every tool call is evaluated in Node.js process space before the filesystem is touched.

blockedPaths    → BLOCK    (hard deny — secrets, system files)
requireApproval → PENDING  (human must approve before execution)
allowedPaths    → ALLOW    (execute immediately, log it)
(default)       → BLOCK    (unknown paths are rejected)

Observe — know what is happening, in real time and historically

The action ledger records every tool call to SQLite with decision, reason, output, and timestamps. The dashboard at http://localhost:47000 is always available. waymark watch gives you an ANSI terminal view. The Agent Monitor (/agents) shows every AI session on the machine — model, context%, tokens, turns, rate limits — as a live btop-style table.

Recover — every write is undoable

Before-snapshots are captured at write time. A single action can be rolled back. An entire session can be atomically undone in reverse order. Approving a write knowing you can roll it back is categorically different from approving without a net.


Related MCP server: safe-code-mcp

60-Second Quickstart

Option A — Zero global install

cd your-project
npx @way_marks/cli init --yes   # create waymark.config.json + CLAUDE.md, register MCP
npx @way_marks/cli start        # start server, open dashboard at localhost:<port>

Restart Claude Desktop or reload your Claude Code session to pick up the new MCP server. Done.

npm install -g @way_marks/cli
waymark global-setup            # register universal MCP entry once across all hosts
cd your-project
waymark init                    # project-specific init (idempotent — safe to re-run)
waymark daemon start            # start one daemon at localhost:47000 for all projects
waymark daemon status           # list registered projects + uptime
waymark status                  # check this project's status + pending count
waymark logs --pending          # show pending actions waiting for approval

The daemon serves all your registered projects from a single process. Switch projects in the dashboard via the Active Project dropdown.


Part of a Suite

Waymark is the control plane of a four-tool developer suite:

Tool

Role

What it does

devloop

Build

Multi-agent dev pipeline — architect → worker → reviewer loop

waymark

Run

Policy enforcement + observability for AI agents

teststop

Break

Adversarial scenario testing — acts as a real, impatient user

docuflow

Document

Decision-context wiki for AI agents

Adversarial testing: teststop runs 52 predicted risk scenarios against waymark's live API, targeting edge cases a real user would hit — concurrent approvals, stale tabs, rollback races, policy bypass attempts. See examples/waymark-demo for the full scenario set and confidence report. The v5.0.13–v5.0.16 safety fixes were all surfaced by this suite.


Platform Support

Platform

Status

Features

Setup

Claude Desktop

✅ Recommended

Full features

waymark init

Claude Code

✅ Recommended

Full features

waymark init

GitHub Copilot CLI

✅ Supported

Full features

waymark init

GitHub Copilot Chat

⏳ Future

Waiting for GitHub MCP

See Platform Guide

CodeWhisperer, Codeium, others

⏳ Future

Waiting for MCP adoption

See Platform Guide

👉 For detailed platform information, see docs/README_PLATFORMS.md


📚 Documentation

Full documentation site: shaifulshabuj.github.io/waymark

Covers installation, quickstart, CLI reference, API reference, policy engine, approvals, rollback, agent monitor, philosophy, and changelog.

User stories and feature walkthroughs: docs/user-stories/README.md


Changelog

See CHANGELOG.md for the full release history.

Recent highlights:

  • v5.0.20 — maxBashTimeoutMs policy: per-call bash timeout clamping via waymark.config.json

  • v5.0.16 — 30+ robustness fixes + complete Vitest migration (624 tests, 33 files)

  • v5.0.13 — 17 P0 safety fixes surfaced by teststop adversarial testing

  • v5.0.0 — Unified daemon architecture: one process, all projects, one URL


Architecture

┌──────────────────────────────────────────────────┐
│                 mcp/server.ts                    │
│                                                  │
│  1. loadConfig()          → policies/engine.ts   │
│  2. checkFileAction()                            │
│     or checkBashAction()                         │
│                                                  │
│  3a. decision=block  → insertAction(blocked)     │
│                         throw Error (no exec)    │
│                                                  │
│  3b. decision=pending → insertAction(pending)    │
│                         notifyPendingAction()    │
│                         return message+action_id │
│                         (agent can poll status)  │
│                                                  │
│  3c. decision=allow  → insertAction(allow)       │
│                         execute tool             │
│                         updateAction(result)     │
└──────────────────────────────────────────────────┘
         │                        ▲
         │ SQLite                 │ REST API
         ▼                        │
┌─────────────┐        ┌──────────────────────────┐
│ data/       │        │      api/server.ts        │
│ waymark.db  │◄───────│  GET  /api/actions        │
└─────────────┘        │  GET  /api/actions/:id    │
                       │  POST /api/actions/:id/   │
                       │        approve            │
                       │  POST /api/actions/:id/   │
                       │        reject             │
                       │  GET  /api/actions/:id/   │
                       │        status             │
                       │  POST /api/actions/:id/   │
                       │        rollback           │
                       │                           │
                       │  [Phase 1: Sessions]      │
                       │  GET  /api/sessions       │
                       │  GET  /api/sessions/:id   │
                       │  GET  /api/sessions/:id/  │
                       │        actions            │
                       │  POST /api/sessions/:id/  │
                       │        rollback           │
                       │  GET  /api/sessions/:id/  │
                       │        status             │
                       │                           │
                       │  POST /api/slack/interact │
                       │  GET  /api/config         │
                       │  GET  /  → index.html     │
                       └──────────────────────────┘
                                  ▲
                          Browser http://localhost:&lt;port&gt;

Three packages, seven server modules, two runtime processes. The dashboard is a separate React + Vite app (packages/web) whose build output is served statically by the API process.

packages/server/ — MCP server + API server + UI host

File

Process

What it does

Imports

packages/server/src/mcp/server.ts

MCP (stdio)

Exposes tools to AI agent, enforces policy, logs actions, sends Slack notifications

db/database, policies/engine, notifications/slack

packages/server/src/api/server.ts

API (default port from 47000-47999)

REST API + serves the built React dashboard from src/ui-dist/. Port set via WAYMARK_PORT env var by the CLI; pinnable via port in waymark.config.json or waymark start --port <n>.

db/database, policies/engine, approvals/handler

packages/server/src/db/database.ts

both

SQLite schema, prepared statements, CRUD functions

better-sqlite3

packages/server/src/policies/engine.ts

both

Loads config, evaluates file/bash actions against rules

micromatch

packages/server/src/notifications/slack.ts

MCP (stdio)

Sends Slack Block Kit message for pending actions; silent if webhook not configured

db/database (ActionRow type)

packages/server/src/approvals/handler.ts

API (port from CLI)

Re-executes approved write_file actions; marks approved/rejected in DB

db/database, fs

packages/server/src/ui-dist/

browser

Vite build output — the React dashboard (produced by packages/web). If missing, the API serves a small setup page pointing to the build command.

—

packages/server/src/api/events.ts

API (port from CLI)

Server-Sent Events broadcaster; emits per-topic events on every mutation so the UI can invalidate without polling.

—

packages/server/src/collectors/

API only (single setInterval, .unref()d)

btop-style agent monitor (added in 4.1). MultiCollector.tick() orchestrates claude.ts / codex.ts / copilot.ts collectors on a 2 s fast tick + 10 s slow tick (ps, lsof, git); applies normalizeSession() to every row (number/array defaults, status clamp, redactSecrets() over free text); JSONL parsing wraps each JSON.parse(line) in try/catch so a corrupted line skips rather than poisons the tick. Outputs CollectorSnapshot { sessions, rateLimits, orphanPorts, collectedAt }.

process.ts, secrets.ts, rate-limit.ts, types.ts

packages/server/src/api/routes/agent-monitor.ts

API (port from CLI)

REST endpoints under /api/agent-monitor/* (sessions, rate-limits, ports, snapshot). Filterable by ?agent= and ?status=.

collectors/multi-collector

packages/server/src/mcp/tools/agent-monitor.ts

MCP (stdio)

Three read-only MCP tools — list_agent_sessions, get_rate_limits, get_agent_ports. Bypass the action log. The MCP process does not run its own collector; fetchSnapshotFromApi() calls http://127.0.0.1:<port>/api/agent-monitor/snapshot on demand (returns an empty snapshot if the API is offline). Single source of truth, no parallel ps polls.

api/routes/agent-monitor (via HTTP)

packages/web/ — React + Vite dashboard (added in 2.0.3)

Area

Purpose

vite.config.ts

Build output goes to ../server/src/ui-dist/; dev server on :5173 with /api proxied to the running Waymark server (set WAYMARK_API env var to override the proxy target)

src/api/

Typed fetch client + TanStack Query hooks (3-second refetchInterval, invalidate-on-success mutations)

src/components/AppShell

232 px sidebar + topbar with Cmd-K search + live MCP status footer

src/components/{Drawer,ActionRow,SessionGroup,Icon,TweaksPopover,ConfirmModal,ToastContext}

Cross-screen primitives

src/features/actions/

Feature-complete Actions screen (filter pills, session groups, approve/reject/rollback)

src/features/agent-monitor/

btop-style live agent view (added in 4.1) — AgentMonitorView, SessionCard, RateLimitBadge, PortsList. Polls /api/agent-monitor/* every 3 s.

src/features/ComingSoon.tsx

Placeholders for Sessions/Approvals/Policy/Stats/Settings (Phases 2–4)

src/store/ui.ts

Zustand store for theme, density, grouping, accent, filter, search, selectedActionId; persisted to localStorage as waymark:ui

src/styles/{tokens,global}.css

oklch-based dark + light tokens, IBM Plex Sans/Mono self-hosted, density variants, semantic colors, responsive breakpoints

packages/cli/ — waymark CLI (init, start, stop, status, logs)

File

What it does

packages/cli/src/index.ts

Entry point — routes process.argv[2] to command modules

packages/cli/src/commands/init.ts

waymark init — creates config, CLAUDE.md (and COPILOT.md when copilot-cli is selected), updates .gitignore, registers MCP in Claude config and ~/.copilot/mcp-config.json

packages/cli/src/commands/agents.ts

waymark agents (added in 4.1) — fixed-column table of live AI coding agent sessions (Claude, Codex, Copilot CLI). Flags: --agent, --active, --json, --limit

packages/cli/src/commands/start.ts

waymark start — spawns API + MCP servers as detached background daemons, writes .waymark/waymark.pid, opens browser, exits immediately

packages/cli/src/commands/stop.ts

waymark stop — reads .waymark/waymark.pid, SIGTERMs both processes, deletes PID file

packages/cli/src/commands/status.ts

waymark status — reads PID file, uses process.kill(pid, 0) liveness check, prints pending count and server state; auto-cleans stale PID file on crash

packages/cli/src/commands/logs.ts

waymark logs — prints action table; supports --pending, --blocked, --limit N

The two server processes run concurrently and share the same SQLite file. The MCP process writes; the API process reads. SQLite handles concurrent access safely because all database calls are synchronous (better-sqlite3).


CLI Reference

waymark init               # first-time setup (idempotent — safe to re-run)
waymark init --yes         # non-interactive, accept all defaults
waymark init --dry-run     # preview what would be created without writing files
waymark global-setup       # register universal MCP entry across all supported hosts
waymark cleanup-mcp        # remove stale per-project waymark-* entries

waymark daemon start       # start global daemon (port 47000, serves all projects)
waymark daemon stop        # stop the daemon
waymark daemon status      # list registered projects + uptime
waymark daemon restart     # reload after upgrading

waymark service install    # auto-start daemon on login (launchd/systemd)
waymark service uninstall  # remove auto-start

waymark start              # start per-project API + MCP server in background
waymark stop               # stop the background servers
waymark status             # check if server is running + pending count + port

waymark logs               # print recent action table
waymark logs --pending     # only pending actions
waymark logs --blocked     # only blocked actions
waymark logs --limit 50    # show up to 50 rows (default: 20)

waymark agents             # live table of AI coding agent sessions
waymark agents --json      # full token / tool / turn breakdown
waymark watch              # live ANSI terminal dashboard (2s refresh)

waymark explain <id>       # human-readable summary of any action
waymark doctor             # validate dependencies and configuration
waymark update             # self-upgrade to latest version
waymark relocate           # update registry after rename/move

waymark global-config init # create global defaults in ~/.waymark/global.config.json
waymark team add <email>   # add a team member
waymark workspace create   # group related projects

waymark.config.json Reference

Waymark reads waymark.config.json from the project root fresh on every tool call — no restart needed when you edit it.

{
  "version": "1",
  "policies": {
    "allowedPaths":    ["./src/**", "./data/**", "./README.md"],
    "blockedPaths":    ["./.env", "./.env.*", "./package-lock.json", "/etc/**", "/usr/**"],
    "blockedCommands": [
      "rm -rf",
      "DROP TABLE",
      "regex:\\|\\s*bash",
      "regex:\\|\\s*sh\\b"
    ],
    "requireApproval": ["./src/db/**", "./waymark.config.json"],
    "maxBashOutputBytes": 10000,
    "maxBashTimeoutMs": 120000
  }
}

Field reference

Field

Type

Description

allowedPaths

string[]

Glob patterns for paths agents may read/write. Relative patterns resolve from the project root.

blockedPaths

string[]

Glob patterns that are always denied. Checked before allowedPaths.

blockedCommands

string[]

Bash rules. Plain strings match as substrings; regex: prefix enables regex matching.

requireApproval

string[]

Glob patterns that are logged as pending and not executed — manual approval required.

maxBashOutputBytes

number

Cap on stdout/stderr bytes returned per bash call (default: 10000). Output beyond this limit is truncated with [OUTPUT TRUNCATED].

maxBashTimeoutMs

number

Per-call bash timeout cap in ms (clamped to 1000–600000). Overrides the MCP client's default.

File action precedence

For every write_file or read_file call, the path is tested in this order:

1. blockedPaths?    → decision: block   (throws error, file not touched)
2. requireApproval? → decision: pending (returns message with action_id, file not touched)
3. allowedPaths?    → decision: allow   (proceeds)
4. (default)        → decision: block   ("Path not in allowedPaths")

blockedCommands: plain string vs regex

// Substring match — blocks any command containing "rm -rf"
"rm -rf"

// Regex match — blocks any pipe to bash (curl url | bash, cat x | bash, etc.)
"regex:\\|\\s*bash"

// Regex match — blocks command substitution with curl
"regex:\\$\\(curl"

Regex rules use JavaScript RegExp with the i (case-insensitive) flag. Invalid regex entries are skipped with a console.warn.

Pattern matching

Relative patterns (./src/**) are resolved to absolute paths from the project root before matching. Absolute patterns (/etc/**) pass through unchanged. Matching uses micromatch with { dot: true } so dotfiles (.env) are included.


REST API Reference

All endpoints are served by api/server.ts on the project's allocated port (default range 47000-47999; pin with port in waymark.config.json or --port flag). Run waymark status to see the live URL.

Method

Path

Response

Notes

GET

/api/actions

ActionRow[]

Last 100 actions, newest first

GET

/api/actions?count=true

{ count: number }

Count of pending actions; used by UI badge

GET

/api/actions/:action_id

ActionRow

Single action by UUID

GET

/api/actions/:action_id/status

{ status, decision, approved_by?, approved_at?, rejected_reason?, rejected_at? }

Lightweight status for agent polling

POST

/api/actions/:action_id/approve

{ success, action }

Executes the original action; sets approved_by='ui'

POST

/api/actions/:action_id/reject

{ success, action }

Body: { reason?: string }; marks rejected, does not execute

POST

/api/actions/:action_id/rollback

{ success, action, message }

write_file only; see Rollback section

POST

/api/slack/interact

{ text }

Slack interactive components endpoint; requires public URL (use ngrok locally)

GET

/api/sessions

{ session_id, action_count, latest }[]

Grouped by MCP server process start

GET

/api/config

WaymarkConfig

Current parsed waymark.config.json

GET

/

HTML

Web dashboard (src/ui/index.html)

Pending action flow

When decision=pending, the MCP server:

  1. Logs the action to DB with status='pending'

  2. Fires a Slack notification (if WAYMARK_SLACK_WEBHOOK_URL is set in .env)

  3. Returns a message to the agent containing the action_id and the polling URL:

    Action requires approval.
    Action ID: <uuid>
    Check status: GET /api/actions/<uuid>/status

    The agent can poll /api/actions/:id/status and continue other work while waiting.

On approve: approvePendingAction re-executes the original tool (writes the file for write_file), sets status='success', decision='allow', and records approved_by + approved_at.

On reject: rejectPendingAction sets status='rejected' and records rejected_reason + rejected_at. The file is never written.

Slack setup

Slack integration is optional. If WAYMARK_SLACK_WEBHOOK_URL is empty, notifications are silently skipped and approval works entirely from the UI dashboard.

There are two independent pieces:

  1. Incoming webhook — posts a notification card when an action goes pending (one-way, read-only setup)

  2. Interactive components — lets you approve/reject directly from the Slack message (requires a public URL)

You can use (1) without (2). Notifications will arrive but the buttons won't work.

Step 1 — Create a Slack App

  1. Go to https://api.slack.com/apps → Create New App → From scratch

  2. Name it Waymark, pick your workspace → Create App

Step 2 — Incoming Webhook (notifications)

  1. In your app's sidebar: Incoming Webhooks → toggle Activate Incoming Webhooks ON

  2. Click Add New Webhook to Workspace → pick the channel (e.g. #engineering) → Allow

  3. Copy the webhook URL — it looks like https://hooks.slack.com/services/T.../B.../...

  4. Add to .env:

WAYMARK_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../...
WAYMARK_SLACK_CHANNEL=#engineering
WAYMARK_BASE_URL=http://localhost:47000

WAYMARK_BASE_URL is used to build the "View in Dashboard" link inside the Slack card. Set it to whatever URL your Waymark UI is reachable at.

Restart the server. The next pending action will post a card to Slack.

Step 3 — Interactive Components (approve/reject buttons)

The Approve and Reject buttons in the Slack card POST to POST /api/slack/interact. Slack requires this endpoint to be publicly reachable over HTTPS.

For local development — use ngrok:

ngrok http 47000

Copy the https:// forwarding URL (e.g. https://abc123.ngrok-free.app).

  1. In your Slack app sidebar: Interactivity & Shortcuts → toggle Interactivity ON

  2. Set Request URL to: https://abc123.ngrok-free.app/api/slack/interact

  3. Click Save Changes

ngrok URLs change on every restart. Update the Request URL in Slack each time.

For production — set a stable public URL:

Deploy Waymark behind a reverse proxy (nginx, Caddy, etc.) with TLS, then set the Request URL once to https://your-domain.com/api/slack/interact. Update WAYMARK_BASE_URL in .env to match.

Step 4 — Verify

  1. Trigger a pending action (write to a requireApproval path via the MCP tool)

  2. A card should appear in your Slack channel with Approve and Reject buttons

  3. Click Approve — the button should update the message and the UI dashboard should show the action as success / allow

Troubleshooting

Symptom

Cause

Fix

No Slack message appears

WAYMARK_SLACK_WEBHOOK_URL empty or wrong

Check .env, restart server

Buttons do nothing

Request URL not set or ngrok restarted

Update Interactivity URL in Slack app settings

dispatch_failed error in Slack

Waymark returned non-200 or timed out

Check server logs; ensure /api/slack/interact is reachable

cannot_parse_subscription

Webhook URL is malformed

Re-copy URL from Slack app settings

Buttons appear but action stays pending

approved_by not set

Check server logs for handler errors


Database Schema

Single table: action_log in data/waymark.db.

Column

Type

Nullable

Description

id

INTEGER

NO

Auto-increment primary key

action_id

TEXT

NO

UUID — unique per tool call

session_id

TEXT

NO

UUID — shared across all calls in one MCP server process

tool_name

TEXT

NO

write_file, read_file, or bash

target_path

TEXT

YES

Absolute file path for write/read; null for bash

input_payload

TEXT

NO

JSON-serialized tool arguments

before_snapshot

TEXT

YES

File content before write (write_file only); null if file didn't exist

after_snapshot

TEXT

YES

File content after write, or file content for reads

status

TEXT

NO

pending → success / error / blocked

error_message

TEXT

YES

Error detail when status=error

stdout

TEXT

YES

bash stdout output

stderr

TEXT

YES

bash stderr output

rolled_back

INTEGER

NO

0 or 1

rolled_back_at

TEXT

YES

ISO datetime of rollback

created_at

DATETIME

NO

Row creation time

decision

TEXT

NO

Policy decision: allow, block, or pending

policy_reason

TEXT

YES

Human-readable reason for the decision

matched_rule

TEXT

YES

The specific rule that matched (e.g. ./.env, rm -rf, regex:|\s*bash)

approved_at

TEXT

YES

ISO datetime when approved

approved_by

TEXT

YES

'ui' or 'slack'

rejected_at

TEXT

YES

ISO datetime when rejected

rejected_reason

TEXT

YES

Human-provided rejection reason

Schema migrations use try { ALTER TABLE ADD COLUMN ... } catch {} — safe to run against existing databases.


Rollback

Rollback is available for write_file actions only. The UI shows a "rollback" or "delete (new file)" button in the Action column.

Overwrite rollback: before_snapshot is non-null → the file existed before. Rollback writes before_snapshot back to target_path.

New file rollback: before_snapshot is null → the file was created from scratch. Rollback calls fs.unlinkSync(target_path) — the file is deleted.

Blocked actions never modify the filesystem, so they have no rollback button.

Pending actions also never modify the filesystem until explicitly approved. After approval, the approved action row shows a ✅ Approved badge instead of a rollback button.

Once rolled back, rolled_back=1 is set and the action cannot be rolled back again.


Plan Mode Logging & Observability

Waymark logs execution-phase tool calls only — not plan-mode reads, not internal reasoning. Only actual MCP tool calls (write_file, read_file, bash) appear in the ledger. This is a protocol-level constraint: MCP has no mechanism for "I'm thinking about this file" vs "I'm executing with this file."


For AI Agents

Key facts for working with this codebase:

Module dependency order (bottom-up):

micromatch, better-sqlite3, express, dotenv, fetch (Node 18+ built-in)
    ↓
packages/server/src/db/database.ts            (no imports from waymark modules)
packages/server/src/policies/engine.ts        (no imports from waymark modules)
    ↓
packages/server/src/notifications/slack.ts    (imports db/database for ActionRow type only)
packages/server/src/approvals/handler.ts      (imports db/database)
    ↓
packages/server/src/mcp/server.ts             (imports database + engine + notifications/slack)
packages/server/src/api/server.ts             (imports database + engine + approvals/handler)

Path resolution — WAYMARK_PROJECT_ROOT: Both database.ts and policies/engine.ts resolve their data/config paths via:

const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
const DATA_DIR = path.join(PROJECT_ROOT, 'data');           // database.ts
const CONFIG_PATH = path.join(PROJECT_ROOT, 'waymark.config.json');  // engine.ts

WAYMARK_PROJECT_ROOT is set by the --project-root CLI argument parsed at the very top of mcp/server.ts before any imports (because database.ts and engine.ts read the env var at module load time). When running via waymark start, process.cwd() is the project directory so the fallback works correctly.

Database is synchronous: better-sqlite3 is used throughout. There is no await on any database call. Do not introduce async database wrappers.

Config is read fresh every call: loadConfig() in engine.ts reads and parses waymark.config.json on every invocation. No caching. Changes to the config file take effect immediately without restarting the MCP server.

MCP server must be restarted to pick up recompiled code: The policy config is hot-reloaded, but the TypeScript-compiled JavaScript is not. After npm run build, restart Claude Desktop (or the MCP process) for engine changes to take effect.

SESSION_ID is a UUID generated once per MCP server process start (const SESSION_ID = uuidv4() at module level in mcp/server.ts). It groups all tool calls within one session.

Environment variables (loaded via dotenv/config as first import in both server files):

  • WAYMARK_PROJECT_ROOT — absolute path to the project being monitored; determines where data/waymark.db and waymark.config.json are read from. Set via --project-root CLI arg or waymark start. Falls back to process.cwd().

  • WAYMARK_SLACK_WEBHOOK_URL — Slack incoming webhook; if empty, Slack notifications are silently skipped

  • WAYMARK_SLACK_CHANNEL — informational only, not used in code (webhook targets its own channel)

  • WAYMARK_BASE_URL — used to build "View in Dashboard" link in Slack messages (default: http://localhost:47000)

Route registration order in api/server.ts is significant: /approve, /reject, /status sub-routes are registered before the /:action_id catch-all GET, otherwise Express would match the sub-routes as action IDs.

Before modifying mcp/server.ts: the policy check must remain before insertAction for blocked/pending cases — these actions must never touch the filesystem. For pending, the Slack notification fires after insertAction with an inline ActionRow object (no DB round-trip).

Before modifying policies/engine.ts: the precedence order (blocked → pending → allowed → default deny) is load-bearing. Changing it changes observable behavior for all callers.

Known gaps:

  • No rate limiting on API endpoints

  • getActions() is hardcoded to LIMIT 100 with no pagination


Documentation

The full documentation lives in docs/. Start at docs/README.md for a grouped index of every guide.

Most-used pages:


Development

npm run build                   # compile both packages (tsc in each workspace)
cd packages/server && npm run dev:api    # API server only (ts-node)
cd packages/server && npm run dev:mcp   # MCP server only (ts-node)
cd packages/server && npm run db:reset  # delete data/waymark.db and recreate schema
npm test                        # run the full test suite (vitest, 624 tests, 33 files)

Building the CLI: cd packages/cli && npm run build compiles to packages/cli/dist/. Test locally with node packages/cli/dist/index.js <command>.

Extending tools: Add new tool schemas to ListToolsRequestSchema handler and a new else if (name === '...') branch in CallToolRequestSchema handler in packages/server/src/mcp/server.ts. Follow the existing pattern: policy check → insertAction → execute → updateAction.


Release Operations

npm run release major|minor|patch

The script validates git state, bumps versions in both packages/cli/package.json and packages/server/package.json, updates CHANGELOG.md, commits, tags, and pushes. GitHub Actions release.yml then publishes to npm and creates the GitHub Release.

Important: Both package versions must always match — the CLI declares a hard dependency on the exact server version.

Manual Release

  1. Bump versions in packages/server/package.json and packages/cli/package.json (both must match).

  2. Update the server dependency reference inside packages/cli/package.json → dependencies["@way_marks/server"] to the same new version.

  3. Regenerate the lock file: npm install --package-lock-only

  4. Add a release/CHANGELOG.md entry for the new version.

  5. Add a root CHANGELOG.md entry.

  6. Commit, then tag: git tag v<version> && git push origin v<version>

Known CI/CD failure: E403 "cannot publish over previously published version"

Cause: The version in either packages/cli/package.json or packages/server/package.json was not bumped before tagging. npm does not allow re-publishing an already-released version.

Fix: Bump both package versions, update the @way_marks/server dependency in packages/cli/package.json, run npm install --package-lock-only, commit the version bump, re-tag, and push.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that enforces runtime governance on AI agent actions — file access, command execution, delegation chains, and permission escalation.
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    A local MCP server that provides controlled repository access with policy-based file filtering, secret redaction, and audit logging for AI coding agents.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    A self-hosted MCP server that enables AI coding agents to read, edit, search, and run code in local projects with human review loops and policy controls.
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    MCP server that provides a security gateway for AI agents, enforcing allow/confirm/deny policies on tool calls and requiring human approval for risky operations, with full audit logging.
    -