@pencilink/gateway
v0.2.3
Published
Pencil Gateway — local daemon that drives Claude Code for tasks assigned in Pencil.
Downloads
754
Maintainers
Readme
@pencilink/gateway
Local daemon that runs Pencil-assigned chats and tasks through your own
Claude Code installation. When your workspace's Agent Runtime is set to
Run on my computer, Pencil queues work for this daemon; the daemon
claims that work, invokes claude -p locally, and sends status/replies
back to Pencil. Pencil never sees your Anthropic credentials and never
relays Anthropic API calls.
Install
Double-click installer (recommended)
Pencil Gateway ships user-scoped installers for macOS and Linux. Neither
installer needs sudo; each installs the gateway under
~/.pencil/service, walks through pairing when needed, starts the service
immediately, and configures it to start again whenever you sign in.
- macOS: extract
pencil-gateway-installer-macos.zip, then double-click Install Pencil Gateway.command. macOS may ask you to confirm opening a downloaded script the first time. - Linux (systemd desktop): extract
pencil-gateway-installer-linux.tar.gz, then double-click Install Pencil Gateway.desktop. Some file managers require Allow Launching on the first run.
The installer asks whether to install future stable releases automatically. Choose No to get a daily desktop notification instead. Re-running the installer is safe and updates the existing installation in place.
Both installers require Node ≥ 20, npm, and a signed-in claude CLI on the
interactive user's PATH.
To build the two distributable archives from this repository:
pnpm installersThe repository's Test Pencil Gateway CI job also publishes both archives
as the pencil-gateway-installers workflow artifact.
npm / command-line install
npm install -g @pencilink/gatewayRequires Node ≥ 20 and a working claude CLI on PATH.
Quick Start
In Pencil, open Settings → Agent runtime and choose Run on my computer.
Click Generate pair code. The code is valid for 15 minutes and can be redeemed once.
Pair this machine:
pencil-gateway pairPaste the code when prompted. The gateway exchanges it for a long-lived device token stored at
~/.pencil/gateway.jsonwith mode0600. The plaintext token is shown to the daemon once; Pencil stores only its hash.Start the daemon:
pencil-gateway startIn an interactive terminal, startup now opens with a multicolor outlined Pencil wordmark, a slim horizontal colored-pencil drawing, and a compact summary of the active account, Pencil URL, machine, Claude authentication, version, and local concurrency. Redirected output and launchd/systemd services keep the existing clean structured logs instead.
Disable the interactive banner when needed:
pencil-gateway start --no-bannerThe daemon heartbeats every 30s and claims dispatches when it has local capacity. When you assign yourself a task in Pencil, it spawns
claude -pin~/.pencil/work/<orgId>/<taskId>/. It runs up to 3 independent dispatches at a time by default while keeping the same task or chat thread serialized.To tune local parallelism:
pencil-gateway start --max-concurrency 4Leave the process running. New eligible chats/tasks will be picked up automatically. Stop it with
Ctrl+C.
Each dispatch attaches the paired Pencil MCP server directly to Claude Code. Agents can read Pencil objects and post reviewable proposal cards with native tools even when local Bash or network commands are not approved.
Self-Hosted Pencil
Use --mc-url when pairing against a self-hosted Pencil instance:
pencil-gateway pair --mc-url https://pencil.example.comThe paired config stores that URL, so later start, status, doctor,
and mcp commands use the same host automatically.
Multiple Pencil accounts
The default pairing still uses ~/.pencil/gateway.json. To connect two
Pencil accounts at the same time, pair each one into a named local
profile and run one daemon per profile:
pencil-gateway pair --account work
pencil-gateway pair --account personal
pencil-gateway start --account work
pencil-gateway start --account personalNamed profiles are stored as ~/.pencil/gateway.<account>.json. Gateway
requests for named profiles also include ?account=<account> so server
logs and future routing can distinguish concurrent daemons; the device
token remains the authority for auth and workspace scope.
To see which profiles you've already paired (and the exact command to start each one), run:
pencil-gateway listExample output:
Paired account profiles (2):
work
MC URL: https://app.pencil.ink
Org/User: org_abc / user_123
Paired at: 2026-07-17T13:00:00.000Z
Start: pencil-gateway start --account work
personal
MC URL: https://app.pencil.ink
Org/User: org_def / user_456
Paired at: 2026-07-17T13:05:00.000Z
Start: pencil-gateway start --account personalUse the same account flag with diagnostics and MCP:
pencil-gateway status --account work
pencil-gateway doctor --account work
pencil-gateway mcp --account workMCP server
The same pairing can expose Pencil's agent/operator tools to any MCP client over stdio:
pencil-gateway mcpThis is a local MCP server. It runs on your machine and communicates
with Pencil through the same paired device token used by
pencil-gateway start. Hosted remote MCP is also available from Pencil
at SITE_URL/api/mcp for connector-style clients; use OAuth there, or
an API key created in Settings with MCP access enabled if your client
supports static bearer headers.
If you only need stdio MCP and do not want to pair the local task runtime, authenticate the gateway through Pencil OAuth instead:
pencil-gateway mcp --oauthThe first run opens Pencil in your browser for sign-in, workspace selection,
and consent. The gateway dynamically registers a public PKCE client and stores
its short-lived access token, rotating refresh token, and client registration
in ~/.pencil/mcp-oauth.json with mode 0600. Later runs refresh the access
token automatically. Named OAuth profiles use
~/.pencil/mcp-oauth.<account>.json:
pencil-gateway mcp --oauth --account workOAuth mode proxies the hosted /api/mcp tool surface, so the selected
workspace and mcp:read / mcp:write scopes are enforced server-side.
Hosted remote MCP intentionally excludes dangerous tools; --oauth cannot be
combined with --include-dangerous.
Example client config:
{
"mcpServers": {
"pencil": {
"command": "pencil-gateway",
"args": ["mcp"]
}
}
}For a named profile:
{
"mcpServers": {
"pencil-work": {
"command": "pencil-gateway",
"args": ["mcp", "--account", "work"]
}
}
}On startup the MCP server reads the selected gateway config
(~/.pencil/gateway.json by default, or
~/.pencil/gateway.<account>.json with --account), fetches the current
tool catalog from Pencil via GET /api/mc/tools, and forwards tool
calls to POST /api/mc/actions with the paired device token. Pencil
keeps enforcing workspace and task-assignment scope server-side.
For task work, clients should use this compact workflow:
- Call
list_taskswith human/agent assignee, roadmap, and status filters to locate the work. It returns resolved assignee summaries, canonical task URLs, and an opaque cursor; done tasks are excluded unless requested. Uselist_membersorresolve_memberfor explicit human-directory lookup. - Call
get_task_contextonce before acting. It returns the task, plan gate, checklist, relationships, recent comments, files, repositories, and completion readiness in one bounded response. - Use
post_commentfor durable discussion andsave_outputfor artifacts. - Move completed implementation to
reviewwithupdate_task_status; usedoneonly when the user explicitly wants final completion.
Pencil injects workspace and caller identity for MCP writes. Do not add
workspaceId, orgId, or agentId to MCP calls. Reuse idempotencyKey when
retrying post_comment, save_output, agent_update, or ask_question so
network retries do not duplicate records.
The v1 MCP surface exposes tools only. MCP resources and prompts are
planned, but are not available from pencil-gateway mcp yet.
Dangerous tools are hidden by default. To request them, the Pencil server
must set PENCIL_MCP_ENABLE_DANGEROUS_TOOLS=1 and the local command must
opt in:
pencil-gateway mcp --include-dangerousFor self-hosted Pencil, pair with --mc-url or set PENCIL_MC_URL
before pairing:
PENCIL_MC_URL=https://your-pencil.example.com pencil-gateway pairOAuth mode can target the same self-hosted instance without pairing:
pencil-gateway mcp --oauth --mc-url https://your-pencil.example.comIf port 8787 is already in use during first-time consent, choose another
loopback port with --oauth-callback-port <port>; the gateway will dynamically
register that exact callback URL.
Commands
| Command | What it does |
|---|---|
| pencil-gateway pair | Redeem a one-time pair code → device token |
| pencil-gateway start | Long-running daemon: heartbeat, claim dispatches, run Claude Code |
| pencil-gateway list | List paired account profiles + a ready-to-run start command for each (alias ls) |
| pencil-gateway mcp | Local MCP stdio server for Pencil tools; add --oauth to use hosted OAuth instead of pairing |
| pencil-gateway status | Show current pairing + a one-shot ping |
| pencil-gateway doctor | Sanity-check claude/server/token and the core MCP task workflow |
| pencil-gateway service install | Install and start a user-level launchd/systemd service |
| pencil-gateway service status | Show whether the startup service is installed and running |
| pencil-gateway service update | Check for and install a newer managed release |
| pencil-gateway service uninstall | Stop/remove startup units; preserve pairing and work files |
| pencil-gateway unpair | Remove the startup service and local pairing config |
All commands accept --account <name> to select a named local profile.
Running as a service
The double-click installer configures the service automatically. For a command-line npm installation, pair first and then run:
pencil-gateway service install
pencil-gateway service statusOn macOS this creates per-user LaunchAgents under
~/Library/LaunchAgents. On Linux this creates systemd user units under
~/.config/systemd/user. The gateway starts at sign-in, restarts after
unexpected failures, and does not run as root.
Named profiles get independent units:
pencil-gateway service install --account work
pencil-gateway service status --account workUpdates
Every service installation includes a daily update check against the stable
latest release of @pencilink/gateway.
- A normal CLI service install sends a desktop notification when a newer release exists. Update a global npm installation first, then reinstall the service so its exact executable path stays current.
- The double-click installer can enable managed automatic updates. It updates
the rootless package under
~/.pencil/serviceand restarts the gateway only after npm succeeds.
Useful commands:
pencil-gateway service update --check
pencil-gateway service update
pencil-gateway service uninstallservice uninstall removes only the startup definitions and service metadata.
It deliberately preserves ~/.pencil/gateway*.json, work directories, logs,
and the installed npm package.
Logs:
- macOS:
~/.pencil/logs/gateway*.log - Linux:
journalctl --user -u pencil-gateway.service
Configuration
| Env var | Default | Notes |
|---|---|---|
| PENCIL_MC_URL | https://app.pencil.ink | Override for self-hosted Pencil. |
| PENCIL_GATEWAY_MAX_CONCURRENCY | 3 | Concurrent local Claude Code dispatches, clamped to 1..8. CLI flag wins. |
| PENCIL_GATEWAY_DEBUG| unset | 1 → emit debug log lines. |
| PENCIL_GATEWAY_NO_BANNER | unset | 1 disables the interactive startup banner. NO_COLOR disables ANSI color while preserving the banner. |
| PENCIL_GATEWAY_AUTO_UPDATE | unset | Double-click installer only: 1 chooses automatic updates; 0 chooses notifications without prompting. |
| PENCIL_GATEWAY_INSTALL_ROOT | ~/.pencil/service | Double-click installer only: override the rootless npm prefix. |
Security model
- One user → one daemon → that user's tasks only. The server refuses
any
/api/mc/actionscall from this daemon that references a task assigned to a different user. - The device token is a long-lived bearer; treat
~/.pencil/gateway.jsonlike an SSH private key. - The local MCP server never stores org-scoped connector credentials such as Notion, Slack, or GitHub OAuth tokens. Pencil keeps those credentials server-side and exposes narrow tools.
- Local Claude dispatches use a strict per-run MCP config containing only the
paired Pencil server. Agent allowlists are applied to the corresponding
mcp__pencil__*tools instead of requiring broad Bash approval. - Hosted remote MCP uses OAuth or explicitly MCP-enabled API keys, binds
OAuth tokens to
SITE_URL/api/mcp, and filters out local-gateway-only, credential-bearing, destructive, dangerous, and sandbox tools. - OAuth-backed local stdio MCP stores its public-client registration and
rotating tokens in
~/.pencil/mcp-oauth*.jsonwith mode0600; it never exposes those credentials to the MCP client process. - Revoke from Pencil → Settings → Gateway. After revoke the daemon's next heartbeat or dispatch claim returns 401 and the daemon exits.
MCP troubleshooting
| Symptom | Check |
|---|---|
| No paired gateway | Run pencil-gateway status; if using a named profile, include --account <name>. |
| Tasks or chats do not start | Confirm the daemon is running, the workspace runtime is Run on my computer, and pencil-gateway doctor passes. |
| Dispatch exits immediately with Invalid API key | The local gateway runs your local claude CLI; Pencil's saved BYO key applies to the direct server runtime and is not copied to the daemon. Fix or unset ANTHROPIC_API_KEY in the shell/service that starts the gateway, run /login inside claude if using a Claude subscription, then restart the gateway. |
| Dispatch fails because the OAuth session expired and could not be refreshed | Run claude auth login as the same OS user that runs Pencil Gateway, then restart the gateway and select Resend. If a service definition sets CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_AUTH_TOKEN, refresh or unset that stale value first. pencil-gateway doctor now reports whether Claude Code is signed in. |
| Agent work routes to the wrong machine | Check the agent operator user in Pencil and which paired user/account is running locally. |
| Too much local load | Lower concurrency with pencil-gateway start --max-concurrency 1 or PENCIL_GATEWAY_MAX_CONCURRENCY=1. |
| No tools appear | Run pencil-gateway doctor, confirm pairing, and restart the MCP client. |
| Calls fail with 401 | Re-pair or revoke the stale device in Pencil settings and pair again. |
| Service is installed but not running | Run pencil-gateway service status, then inspect the platform log above. Confirm the captured Node and Claude Code installation still exist. Re-run the installer after changing Node managers. |
| Linux installer will not launch | Extract both files together, mark Install Pencil Gateway.desktop as trusted/Allow Launching, then double-click again. You can also run bash install-pencil-gateway.sh in a terminal. |
| Update notification appears but update fails | Re-run the double-click installer. Global npm service installs intentionally require npm install -g @pencilink/gateway@latest followed by pencil-gateway service install. |
| Tools call the wrong host | Re-pair with --mc-url <url> or PENCIL_MC_URL=<url>; commands use the URL saved in the selected config. |
| Dangerous tools are missing | Enable PENCIL_MCP_ENABLE_DANGEROUS_TOOLS=1 on the Pencil server and start with --include-dangerous. |
| A client asks for a remote MCP URL | Use SITE_URL/api/mcp with OAuth, or with an MCP-enabled API key if the client supports static bearer headers. |
Building from source
pnpm install builds dist/ automatically (via the prepare script), so
after cloning you can go straight to the convenience scripts:
pnpm install # installs deps + builds dist/
pnpm doctor # sanity-check claude/server/token
pnpm pair # pair against https://app.pencil.ink
pnpm start # run the daemonFor a self-hosted Pencil running locally on port 3030, use the :local
variants (they set PENCIL_MC_URL=http://localhost:3030 for you):
pnpm pair:local
pnpm start:localEvery command accepts extra flags appended directly, e.g.:
pnpm start --max-concurrency 4 --account work
pnpm pair --mc-url https://pencil.example.comAvailable scripts: pnpm start, pnpm start:local, pnpm accounts
(lists paired profiles), pnpm pair, pnpm pair:local, pnpm status,
pnpm doctor, pnpm mcp, pnpm unpair.
Rebuild manually anytime with pnpm build.
