Arman-Luthra/aftr
Puppeteer for After Effects. Use AE with Claude Code to make production-ready videos. - Arman-Luthra/aftr
5 stars · 0 forks · JavaScript Topics: nodejs, automation, video, ffmpeg, cep, mcp, extendscript, after-effects, motion-graphics, vfx, aerender, claude-code
README
aftr Puppeteer for After Effects Use After Effects with Claude Code to make production-ready videos.
A Node controller sends JSON commands over a WebSocket to a CEP panel inside AE, which runs them as ExtendScript and returns JSON. On top sits an autonomous, spec-driven pipeline that builds clips and self-corrects them by rendering, reviewing, and revising. Everything is driven programmatically over the socket, with no manual After Effects work.
A title sequence built and rendered entirely through aftr. No manual After Effects work.Video made for tilion / fortress.Watch the full demo
Get the MCP running Wire aftr into Claude Code in two commands. Start the controller (it serves the MCP endpoint), then point Claude Code at it. npx aftr-studio controller # no install, needs Node 18+ pip install aftr-studio && aftr controller # pip docker run -p 8787:8787 ghcr.io/arman-luthra/aftr # docker claude mcp add --transport http aftr http://127.0.0.1:8787/mcp Open After Effects and the panel (Window > Extensions > aftr; deploy it once from a clone with npm run deploy:panel). Then ask Claude Code in plain language:
Create a 1080p 5s comp, add a title "LAUNCH" with fire behind it, animate blurFade, then render to mp4.
Claude Code sees every command as a tool: it calls ae_status first, lists the set with ae_list_commands, and runs anything through ae_command. Prefer a stdio server? Use claude mcp add aftr -- npx -y aftr-studio mcp (keep the controller running too).
Install
Method Command
npm (global CLI) npm install -g aftr-studio, then aftr controller
npx (no install) npx aftr-studio controller
pip (Python launcher) pip install aftr-studio, then aftr controller (needs Node 18+)
Docker docker run --rm -p 8787:8787 ghcr.io/arman-luthra/aftr
From source clone this repo (below)
The npm, npx, and Docker paths run the controller, MCP server (aftr mcp), and simulator (aftr sim). Deploying the CEP panel into After Effects is done from a clone (npm run deploy:panel), since it self-signs and installs the extension. The pip package is a thin launcher that forwards to the npm CLI through npx.
Get started in 60 seconds git clone https://github.com/Arman-Luthra/aftr.git cd aftr npm install
1) prove it's healthy with NO After Effects (headless simulator + tests)
npm run build:jsx && npm test2) install the panel into AE (self-signs + deploys on Windows and macOS)
npm run deploy:panel # then quit and relaunch AE, Window > Extensions > aftr3) start the bridge + open the UI
npm run controller # http://127.0.0.1:8787 Then drive it from the UI, REST, WebSocket, or as a library: node examples/flaming-title.mjs "ON FIRE" Requires After Effects 2024 to 2026, Node 18+, and ffmpeg on your PATH. Full Windows and macOS setup is in section 4.Features
Capability What it does
~100 commands Comps, layers, keyframes with easing, expressions, effects (by matchName or display name, including deeply nested params and third-party plugins), masks, layer styles, text animators, render.
Discovery listFonts, listInstalledEffects (returns matchNames), findEffectMatchName, listPlugins, getEnvironment. Find what's installed instead of guessing.
One-call VFX fireEffect, smokeEffect, glitchEffect, neonGlow, cinematicGrade, and a friendly applyLumetri grade.
Text animation Four Animate-panel presets (wordReveal, charScale, bunchRotate, blurFade) plus a full range-selector builder.
Autonomous pipeline Declarative segment specs realize, render, get a visual review, take a structured delta, re-render, and concat. It self-corrects.
MCP server Drive AE from Claude Desktop, Claude Code, or any MCP client. Every command is a tool (npm run mcp).
Fast and batched batch runs N edits in one round-trip and one undo (sub-200 ms for 20 ops). Non-blocking aerender streams progress events, with a parallel render engine.
Testable without AE A headless simulator runs the real JSX against a mock DOM. 96 tests plus e2e, CI on Node 18, 20, and 22.
Cross-platform Windows and macOS, one-command sign and deploy.
How it works ┌──────────────────────────────────────────────┐ │ Controller (Node + ws + Express + web UI) │ <- you / the UI / an agent drive this │ • hosts a WebSocket server (panel + agents) │ │ • REST surface + interactive UI │ │ • the HLD orchestrator (agentic loop) │ └───────────────┬────────────────────────────────┘ │ WebSocket (JSON request/response/event envelopes) ┌───────────────▼────────────────────────────────┐ │ CEP panel (inside After Effects) │ ┐ │ • WebSocket CLIENT, dials the controller │ │ build once, │ • routes commands to the JSX dispatch layer │ │ identical on Win & Mac │ • spawns aerender for non-blocking renders │ │ └───────────────┬────────────────────────────────┘ │ │ evalScript (string in / JSON string out) ┌───────────────▼────────────────────────────────┐ │ │ JSX command layer (host.jsx + commands/*.jsx) │ │ │ • dispatch(command, paramsJson) -> JSON │ │ │ • ~90 commands: layers, keyframes, effects, │ │ │ text animators, masks, VFX presets, render │ │ └───────────────┬────────────────────────────────┘ ┘ │ AE scripting DOM (app.project, comps, layers) ┌───────────────▼────────────────────────────────┐ │ After Effects │ └──────────────────────────────────────────────────┘
Table of contents
What you can do with it Prerequisites Quick start, no AE needed (the simulator) Full setup with real After Effects
Windows macOS
Run order Driving the bridge Command vocabulary VFX and text presets The orchestrator (autonomous pipeline) Configuration Dev workflow (hot reload + signing) Architecture Project structure Testing Troubleshooting Cross-platform notes
1. What you can do with it Control AE from code: create comps and layers (solids, text, shapes, nulls, cameras, lights, adjustment, footage), set any transform or effect property, add keyframes with easing, write expressions, and parent, trim, move, or duplicate layers, all over a socket. Add any effect by matchName and drive any of its parameters, even deeply nested ones, animated via keyframes or expressions. Animate text with four ready-made Animate-panel styles (wordReveal, charScale, bunchRotate, blurFade) plus a full text-animator builder (Based On, Shape, Ease High to Low, keyframed Offset and Start). Reach for one-call VFX presets: fireEffect (realistic fire), smokeEffect, glitchEffect, neonGlow, cinematicGrade, and a friendly applyLumetri grade. Batch and introspect: run many edits in one round-trip and one undo (batch), then read back layer or comp state (getLayerDetails, getProperty). Render without blocking: render spawns aerender and streams progress and renderComplete events, so AE stays responsive. Run the autonomous pipeline: describe a video as JSON segments, and the orchestrator realizes each one (applySpec), renders it scoped, reviews it visually, applies a structured spec delta if it's wrong, re-renders until it passes, then concatenates the result with FFmpeg.
2. Prerequisites
Requirement Notes
Adobe After Effects 2024 (24.x), 2025 (25.x), or 2026 (26.x). Licensed and activated. Record your exact version.
Node.js 18+ The controller, simulator, and build scripts are Node (ESM).
FFmpeg Needed by the orchestrator's visual reviewer and final concat. ffmpeg must be on your PATH.
Git To clone and push.
Check them: node --version # v18+ (v22 recommended) ffmpeg -version # any recent build CEP version maps to AE version: AE 2024 uses CEP 11 (CSXS.11); AE 2025 and 2026 use CEP 12 (CSXS.12). The panel manifest targets [24.0,99.9] and a low required runtime, so it loads on all of them.
3. Quick start, no AE needed (the simulator) You can validate the entire architecture (controller-to-bridge protocol, command dispatch, render plumbing) without After Effects, using the headless simulator. It is a Node process that speaks the exact same WebSocket protocol as the real panel and runs the real bundled JSX against a mock AE DOM. git clone https://github.com/Arman-Luthra/aftr.git cd aftr npm install
build the JSX bundle once
npm run build:jsxrun the full headless test suite (unit + simulator + JSX dispatch)
npm testend-to-end: controller + simulator round-trip, incl. render events
npm run e2e Or run it interactively:terminal 1: controller (WS server + REST + UI on http://127.0.0.1:8787)
npm run controllerterminal 2: the simulator (pretends to be the AE panel)
npm run simnow open http://127.0.0.1:8787 and click commands, or:
curl -X POST http://127.0.0.1:8787/command \ -H "Content-Type: application/json" \ -d '{"command":"createComp","params":{"name":"Main","width":1920,"height":1080}}' This is how M0, M3, and M4 (architecture, round-trip, vocabulary) are validated with zero AE. The real panel drops in unchanged for M1, M2, and M5.4. Full setup with real After Effects The panel is a CEP extension. On modern AE (2025+), the old trick of enabling unsigned panels via PlayerDebugMode is unreliable: AE ignores it and rejects unsigned extensions with "Signature verification failed." The reliable, supported path is to self-sign and install the panel, which this repo automates with one command: npm run deploy:panel deploy:panel builds the JSX bundle, creates a self-signed dev certificate (cached in dist/), signs panel/ into a .zxp, and unpacks the signed extension into your per-user CEP extensions folder. It works on both Windows and macOS (the zxp-sign-cmd tool ships the signer for both). After it runs, fully quit and relaunch After Effects, then open Window > Extensions > aftr. A signed extension is a snapshot. After you edit panel source, re-run npm run deploy:panel (and reopen AE) to make it permanent, or use the hot-reload dev loop (see section 11) while iterating. 4.1 Windows
from the repo root
npm install npm run deploy:panelthen quit AE, relaunch, Window > Extensions > aftr
The signed panel installs to: %APPDATA%\Adobe\CEP\extensions\com.ae-bridge.panelYou may also set debug mode for remote DevTools, but it is not required once signed: reg add HKCU\Software\Adobe\CSXS.12 /v PlayerDebugMode /t REG_SZ /d 1 /f (use CSXS.11 for AE 2024). 4.2 macOS
from the repo root
npm install npm run deploy:panelthen quit AE, relaunch, Window > Extensions > aftr
The signed panel installs to: ~/Library/Application Support/Adobe/CEP/extensions/com.ae-bridge.panelmacOS specifics:
If the signer binary is quarantined by Gatekeeper, clear it once: xattr -dr com.apple.quarantine node_modules/zxp-sign-cmd (path may vary). aerender lives at the app root: /Applications/Adobe After Effects <year>/aerender. The controller and panel auto-detect it. If your install is non-standard, set AE_BRIDGE_AERENDER (see section 10). Optional debug mode: defaults write com.adobe.CSXS.12 PlayerDebugMode 1 (use CSXS.11 for AE 2024). Not required when signed.
5. Run order
Start the controller (hosts the WS server, REST, and UI on a fixed port): npm run controller # http://127.0.0.1:8787
Launch After Effects. Open the panel: Window > Extensions > aftr. It dials the controller and the status dot goes green (AE v26.x). Open the UI at http://127.0.0.1:8787: buttons per command, a live event log, and a JSON response viewer.
To run an autonomous demo (see section 9): npm run orchestrate, npm run orchestrate:fire, or npm run showreel.
6. Driving the bridge There are several equivalent ways to send commands, all using the same JSON envelope. REST (one-shot): curl -X POST http://127.0.0.1:8787/command \ -H "Content-Type: application/json" \ -d '{"command":"createComp","params":{"name":"Hello","width":1920,"height":1080,"duration":6,"frameRate":30}}' WebSocket (programmatic, for agents): connect to ws://127.0.0.1:8787/agent, send {"id":"1","type":"command","command":"ping","params":{}}, and receive {"type":"result","id":"1","ok":true,"result":{...}}. Panel events (ready, log, progress, renderComplete) are broadcast to every connected agent. The web UI at / renders a control per command (with a JSON-params box for the rich ones), a live log, and the last response. As a library, the bundled agent client makes scripting simple. See examples/: node examples/flaming-title.mjs "ON FIRE" # build a flaming title programmatically node examples/animated-text.mjs # the four text-animation presets in one batch import { AgentClient } from './controller/src/orchestrator/agentClient.js'; const ae = new AgentClient(); await ae.connect(); const { compId } = await ae.must('createComp', { name: 'Hi', width: 1080, height: 1080, duration: 5, frameRate: 30 }); await ae.must('fireEffect', { compId }); await ae.must('addTextLayer', { compId, text: 'ON FIRE', fontSize: 150, justification: 2, position: [540, 300], name: 't' }); await ae.must('applyTextPreset', { compId, layer: 't', preset: 'blurFade' }); As an MCP server Drive After Effects from any MCP client (Claude Desktop, Claude Code, and others). The MCP server (controller/src/mcp.js) is a thin stdio adapter that forwards each tool call to the running controller, so the AE panel, controller, and your project all behave exactly as above; the client just gets tools.
1) start the bridge and open the AE panel (Window > Extensions > aftr)
npm run controller2a) Claude Code: register the MCP server
claude mcp add aftr -- node /ABSOLUTE/PATH/aftr/controller/src/mcp.js // 2b) Claude Desktop: claude_desktop_config.json { "mcpServers": { "aftr": { "command": "node", "args": ["/ABSOLUTE/PATH/aftr/controller/src/mcp.js"], "env": { "AE_BRIDGE_URL": "http://127.0.0.1:8787", "AE_MCP_TOOLS": "core" } } } } The client then sees:ae_status: is AE connected? (call this first if anything returns NO_PANEL) ae_list_commands: every command and description ae_command: run any command ({ command, params }), the universal escape hatch ae_createComp, ae_addTextLayer, ae_deepGlow, ae_shadowStudio, ae_applySpec, ae_render, and other curated per-command tools
Env Default Meaning
AE_BRIDGE_URL http://127.0.0.1:8787 controller to forward to
AE_MCP_TOOLS core core is the curated set plus meta tools; all exposes every registered command as its own tool
AE_BRIDGE_ALLOW_DEV (unset) 1 to surface dev-only commands
The MCP server is a client of the controller, so run npm run controller (not a second one) to avoid a port clash; the MCP process just needs to reach it. Host it for others: video in, render, video out Run the bridge on your machine and let other people send a video, have your AE render it, and download the result, over HTTP or MCP.
bind to your network and require a shared secret (do BOTH before exposing it)
AE_BRIDGE_HOST=0.0.0.0 AE_BRIDGE_TOKEN=your-secret npm run controllerRoute Purpose
GET /studio a tiny browser page: pick a file, upload, see and download outputs
POST /media/upload?name=clip.mp4 send a video (raw body), returns { id, file, path, url } (lands on the host)
POST /media/fetch { url | dataBase64, name? } host pulls a video by URL (easiest for remote callers)
GET /media/list list incoming and rendered files, each with a download URL
GET /media/file/<name> download a video
POST /media/render { compId, name? } render, transcode to H.264 .mp4, returns { downloadUrl }. Waits up to AE_BRIDGE_RENDER_WAIT_MS (default 150s); longer renders return { pending, jobId }
GET /media/render/<jobId> poll a long render, returns { status, downloadUrl } when done
Renders are tracked as jobs, so a long render never blows the MCP client's ~180s call timeout, and every output is transcoded to a universally playable H.264 .mp4 (via ffmpeg, auto-detected or set with AE_BRIDGE_FFMPEG).
send a clip, then (after building a comp from it) render and fetch the result:
curl -X POST --data-binary @in.mp4 -H "Content-Type: application/octet-stream" \ "http://HOST:8787/media/upload?name=in.mp4&token=your-secret" curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer your-secret" \ -d '{"compId":3,"name":"out.mp4"}' http://HOST:8787/media/renderreturns { "downloadUrl": "/media/file/out.mp4" }
Expose it to the internet with a tunnel (no router config): cloudflared tunnel --url http://localhost:8787 # or: ngrok http 8787 Remote MCP works two ways. The controller also serves MCP over HTTP at POST /mcp (Streamable HTTP), so the tunnel URL is a pluggable MCP link:(a) direct HTTP MCP: paste the URL into any MCP client
claude mcp add --transport http aftr https://your-tunnel.example/mcp \ --header "Authorization: Bearer your-secret" // (b) stdio adapter pointed at the tunnel { "mcpServers": { "aftr": { "command": "node", "args": ["/path/aftr/controller/src/mcp.js"], "env": { "AE_BRIDGE_URL": "https://your-tunnel.example", "AE_BRIDGE_TOKEN": "your-secret" } }}} Remote callers get the video tools too: ae_upload_video ({ url }, host fetches it), ae_media_info, ae_media_list, ae_render_and_download ({ compId }, returns a download URL or { pending, jobId } for long renders), and ae_render_result ({ jobId } to poll). This exposes programmatic control of your After Effects. Always set AE_BRIDGE_TOKEN before binding to 0.0.0.0 or tunneling. With a token set, /command, /media/*, /mcp, and the /bridge and /agent WebSockets all require it (the local AE panel is exempted as a direct-loopback connection). Only share the token with people you trust. Onboarding teammates? Hand them docs/TEAM-GUIDE.md, a no-repo-needed guide to connecting, the tool catalog, and copy-paste recipes (including URL to rendered video in two calls). Message contract // request (controller to panel) { "id": "uuid", "type": "command", "command": "createComp", "params": { ... } } // result (panel to controller) { "id": "uuid", "type": "result", "ok": true, "result": { ... } } { "id": "uuid", "type": "result", "ok": false, "error": "width must be a positive integer" } // event (panel to controller, unsolicited) { "type": "event", "event": "ready", "data": { "ae": "26.3", "project": "Untitled" } } { "type": "event", "event": "progress", "data": { "jobId": "r1", "percent": 42 } }7. Command vocabulary ~90 commands, grouped. List them live at GET /api/commands. Highlights: Project and comp: ping, getProjectInfo, getAppInfo, listComps, createComp, setCompSettings, addCompMarker, duplicateComp, setActiveComp, setCompTime, getCompTime, setWorkArea, clearComp, createFolder, moveToFolder, renameItem, deleteItem, setProxy, getProjectItems, saveProject. Layers: addSolid, addTextLayer, addNull, addAdjustmentLayer, addCamera, addLight, addShape, addPathShape (custom bezier paths), addFootageLayer, setLayerProperty, setParent, trimLayer, moveLayer, duplicateLayer, deleteLayer, getLayers, setBlendMode, setTrackMatte, setLayerFlag, addLayerMarker, setTimeStretch, enableTimeRemap, replaceSource, alignLayer, sequenceLayers. Animation: setKeyframe, setKeyframes (bulk + ease), setEase, setInterpolation, removeKeyframes, setExpression, removeExpression, enableExpression. Effects: addEffect (by matchName), setEffectParam, listEffects, addExpressionControl. Any nested effect param is reachable via a property path through setLayerProperty or setExpression, for example ["ADBE Effect Parade","FN","ADBE Fractal Noise-0012"]. Text: setTextDocument, addTextAnimator (full Animate-panel range selector), applyTextPreset. Masks and styles: addMask, addRectMask, setMaskProperty, addLayerStyle, setLayerStyleEnabled. Footage and render: importFootage, compFromFootage, render (non-blocking aerender, scoped startFrame and endFrame), addToRenderQueue, listRenderQueue, setOutputModule, clearRenderQueue. Grading and VFX presets: applyLumetri, lumetriParams, cinematicGrade, fireEffect, smokeEffect, glitchEffect, neonGlow. Discovery (read-only): li

