AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Automate real Claude Code and Codex sessions in headless PTYs. Interactive sessions that use your regular subscription, not usage-based billing.
Developer toolsTypeScript · JavaScriptGitHub @with-logicOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Automate real Claude Code and Codex sessions in headless PTYs.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Elwood
elwood.bot
Automate real Claude Code and Codex sessions in headless PTYs.
Write scripts that control normal interactive Claude Code and Codex sessions,
which use your regular subscription rather than metered API billing. Elwood
starts the actual agent CLI in a hidden terminal and types into it the way you
would. The agent keeps its tools, hooks, configuration, and login.
Use autotrust only for workspaces and extensions you trust. Otherwise, run the
agent directly in the same directory first and approve its trust prompts.
The agent should not know it is wrapped.Read the full README ↗Automate real Claude Code and Codex sessions in headless PTYs.
Write scripts that control normal interactive Claude Code and Codex sessions, which use your regular subscription rather than metered API billing. Elwood starts the actual agent CLI in a hidden terminal and types into it the way you would. The agent keeps its tools, hooks, configuration, and login.
npm install -g @with-logic/elwood
elwood "Explain this project's architecture."
import { ClaudeSession } from "@with-logic/elwood";
const session = new ClaudeSession({
cwd: "/path/to/project",
autotrust: true, // Approve allowlisted trust prompts for this known project.
});
try {
console.log(await session.send("Explain this project."));
} finally {
await session.close();
}
Use autotrust only for workspaces and extensions you trust. Otherwise, run the
agent directly in the same directory first and approve its trust prompts.
The agent should not know it is wrapped.
Headless agent APIs give you a different product than the CLI your team actually uses: different tools, different hooks, different billing. Elwood drives the real interactive CLI instead. The process stays interactive, runs in your normal shell environment, and is controlled through the hooks and transcripts the CLI already provides.
send() returns a string. stream()
yields typed events. The CLI keeps its own tools and history.elwood
command for shell scripts and pipelines.prd/ is the source of truth for observable behavior. If this README, the
tests, or the implementation disagree with the PRD, the PRD wins.
unsupported_platform.2.1.144 or newer, and/or Codex CLI 0.124.0 or newer,
installed and authenticated.An unparseable CLI version becomes a version_unparseable warning and startup
continues. Pass strictVersionCheck: true to fail closed instead.
npm install @with-logic/elwood
The package installs an elwood executable for the headless CLI. It ships a
prebuilt dist/, so a global install runs no build step and no install scripts
of its own. To work from a checkout instead, run
npm install && npm run build && npm link; the build is explicit because
dist/ is not committed.
import { ClaudeSession } from "@with-logic/elwood";
// Construct synchronously; the session starts lazily on the first turn.
const session = new ClaudeSession({ cwd: "/path/to/project", autotrust: true });
// `send` returns the assistant's reply as a string; a follow-up keeps context.
const summary = await session.send("Summarize this repo's test strategy in two sentences.");
const risks = await session.send("Now list the biggest gaps you'd address first.");
// `stream` yields typed events as they happen.
for await (const event of session.stream("Run the test suite and report failures.")) {
if (event.type === "text") process.stdout.write(event.text);
// event.type is also "thinking" | "tool_call" | "tool_result"
}
await session.close();
CodexSession has the same shape. autotrust: true lets Elwood answer its
allowlisted workspace and extension trust prompts on your behalf; see
Security and privacy for the full scope. Leave it off
when a human should make those decisions. There is no default turn timeout; pass
send(prompt, { timeoutMs }) (a TurnOptions field) to bound one turn.
elwood "Summarize this repository"
git diff | elwood --agent claude "Review this diff for correctness"
elwood --output json "Run the tests and summarize failures" | jq -r .response
elwood --output json "Remember that the release color is teal"
elwood --resume <sessionId> "What is the release color?"
When nothing selects an agent, Elwood tries claude and then codex in your
login shell and uses the first that resolves; if neither does, the run fails
with no_agent_found. See docs/cli.md for configuration,
output protocols, head mode, and continuation.
Sessions. A session is one agent process in one PTY, identified by an
elwoodSessionId. ClaudeSession / CodexSession add send, stream, and
close on top of the full control surface (sendMessage, sendPrompt,
sendGuidance, sendKeys, interrupt, compact, listModels, setModel,
listQuestions, answerQuestions,
loops, waitForStatus, stop, kill, teardown). The eager startClaude /
startCodex factories are deprecated but still available.
Hooks. Claude and Codex hook events are delivered to typed handlers you pass at start. Elwood runs a local bridge that the CLI's hook scripts call over IPC with a per-session token. Handlers can allow, deny, block a stop, or add context; they fail open on error.
Activity stream. The activity event is one adapter-neutral timeline:
assistant messages, reasoning, tool calls and results, web searches, hooks,
warnings, and lifecycle. Most host UIs render this and nothing else.
Codex commentary arrives as assistant_progress with normalized text; final
replies remain assistant_message. Render progress while keeping the turn
working. Progress is available through on("activity", ...), and is excluded
from ergonomic send() results and stream() text. See
Codex commentary for the contract.
Agent questions. Subscribe to question events or question activities to
show native Claude and Codex questionnaires. listQuestions() returns available
requests; answerQuestions() submits the user's choices or text through the
native editor, including while send() is waiting. Queued Codex questions can
remain available while the agent works. See native questions.
Terminal stream. terminal:data carries the raw PTY bytes so a host can
mirror the agent in its own xterm. A late subscriber first receives up to
128 KB of replayed history as one chunk. sendKeys(Uint8Array) forwards a
user's keystrokes verbatim.
Status lifecycle. status moves through starting, ready, running,
and blocked (a dialog needs a human) and ends in stopped, exited,
killed, or torn_down. Every real turn is a running to ready beat.
State directory. Elwood keeps a minimal session record under
<cwd>/.elwood by default, or under an explicit stateDir. It holds what
resume needs and nothing else.
Add --head when you want to watch the real agent interface while retaining a
pipeline-friendly final result:
elwood --head "Run the test suite and fix the failure"
elwood --head --output json "Review this repository" >result.json
elwood --head --agent claude "Explain the architecture"
The display stays in the current terminal and receives the raw PTY byte stream on stderr. It is a real VT/ANSI mirror, so full-screen layouts, cursor-addressed updates, alternate-screen buffers, colors, spinners, and title changes render as they do in the underlying Claude or Codex client. Stdout remains only the final text or JSON protocol and can be redirected independently. Pending display work is capped at 4 MiB or 1,024 frames; a terminal that remains backpressured fails the run cleanly after draining accepted bytes instead of growing memory without bound.
Head mode is deliberately view-only: while attached, ordinary keyboard, mouse,
paste, and terminal-response bytes are discarded; Ctrl-C still interrupts using
Elwood's normal first-interrupt/repeated-force-kill lifecycle. Terminal resizes
propagate to the agent. Elwood restores raw/cooked input state, mouse and paste
modes, attributes, cursor visibility, and the main screen before printing the
final result. Both stdin and stderr must be terminals, and --head cannot be
combined with --stream, --verbose, --debug, or --output jsonl.
New and resumed headless runs preserve Elwood state by default. A resumable
session ID is included in the terminal record for JSON and JSONL. Text output
prints only the agent response; use --show-session-id for an ID footer on
stderr or elwood sessions to list retained sessions. --keep explicitly selects the default; use --ephemeral for
one-off runs that should remove their Elwood state afterward:
first=$(elwood --output json "Remember that the release color is teal")
id=$(printf '%s' "$first" | jq -r .sessionId)
elwood --resume "$id" "What is the release color?"
elwood --resume "$id" --ephemeral "Finish this conversation"
Resume uses the exact stored agent and workspace; conflicting --agent and any
--cwd are rejected. Resumed sessions stay preserved after success or failure unless
--ephemeral requests teardown. Ephemeral teardown removes Elwood's session
record and loop definitions; it does not undo workspace changes or delete
conversation history owned by Claude or Codex. CLI state defaults to absolute
$XDG_STATE_HOME/elwood or ~/.local/state/elwood; its private records contain
resume metadata, not ordinary prompts or output.
Before launching, elwood config effective [run options] prints the resolved
settings and where each came from. The JSON includes head and, for Claude,
the effective permissionMode, allowedTools, disallowedTools, and tools;
resume inspection reports persisted tool rules with stored-session provenance.
Exit status is 0 for success or a closed consumer, 1 for agent/runtime or
cleanup failure, 2 for usage/configuration failure, 124 for timeout, and
130 for interruption. The first Ctrl-C requests a clean interrupt; a repeated
Ctrl-C force-kills before cleanup.
--persona runs an actual extra agent turn before the requested turn. Elwood
discards that setup turn's answer, but any tools it invokes or files it changes
remain. It is therefore supported only for new sessions, never --resume.
elwood sessions lists the Elwood-owned session records in the effective state
directory without starting an agent, and elwood resume <id> is the subcommand
form of --resume <id>:
elwood sessions
elwood sessions --state-dir ./state --output json | jq -r '.sessions[] | .id'
elwood resume "$id" "What is the release color?"
git diff | elwood resume "$id" --ephemeral "Does this diff match what we agreed?"
The table (or the sessions array in JSON) reports each session's id, agent,
workspace, createdAt/lastUsedAt timestamps, whether it is resumable
(the agent's own conversation id is stored), and live — whether a launch's
bridge socket file is currently present. live is a cheap file-presence
signal: a socket left behind by a force-killed owner reads as live until that
session is next started or torn down. Unreadable records are skipped with a
stderr warning, and an empty state directory is a normal empty result.
resume <id> follows every --resume rule: stored agent and workspace, no
--cwd, no persona, and piped stdin composes the same way.
elwood interactive [id] opens the real claude or codex TUI in the current
terminal — no hidden PTY, no automation, no observation, no Elwood state — with
Elwood's resolved agent, model, reasoning effort, workspace, and posture passed
as the agent's own flags. It exits with the agent's exit status and needs a
terminal on stdin and stdout:
elwood interactive # the configured agent, its own defaults
elwood interactive --agent claude --model opus --claude-permission-mode plan
elwood interactive "$id" # the stored conversation, workspace, and posture
Built-in headless posture defaults (Claude dontAsk, Codex workspace-write
and never) are not applied unless a flag, environment variable, config key,
or stored record selected them, so an unconfigured launch is the agent's own
default experience. A session kept by a headless run does store its headless
posture; pass --claude-permission-mode default (or the Codex flags) to get the
agent's prompts back when resuming one interactively. Because the agent owns
the resumed conversation, a later elwood resume <id> sees what you said. The
command rejects --output json|jsonl, --stream, --verbose, --debug,
--head, --timeout, --persona, --image, --keep, --ephemeral, and
--resume.
elwood models lists Claude and Codex models, with each model's agent noted.
It probes Claude then Codex, briefly opening and cancelling each model picker,
and removes both probe sessions afterward. --agent limits the listing to one
agent; saved configuration and ELWOOD_AGENT do not narrow the bare command.
Text marks the current model with * and the default with (default).
elwood models
elwood models --output json | jq '.agents[] | {agent, models}'
elwood models --agent claude --output json | jq -r '.models[] | select(.isCurrent) | .id'
JSON is one {"schemaVersion":1,"type":"models",...} document. Combined
listings contain agents: [{agent, models}] and errors: [...]; explicit
--agent retains the single agent and models fields. If one agent fails or
is unavailable, the other catalog remains available and the command exits
nonzero. --timeout is one budget shared by both probes, and Ctrl-C stops
further probes. Workspace, model, reasoning, trust, state-directory, and
matching adapter posture settings apply; the configured model stays unchanged.
Headless mode is non-interactive. With the default --trust, Elwood answers only
its allowlisted workspace-directory and extension trust dialogs. Codex hook
trust is bypassed session-wide, including hooks beyond Elwood’s own hooks. --no-trust disables the
workspace/extension approvals. Any other recognized dialog fails safely as
blocked_prompt instead of hanging or guessing.
Piped text and image paths are prompt input with the same authority as text typed by the caller. Do not combine untrusted input with broad filesystem permissions. Global config is never loaded from the repository being opened.
Head mode renders agent-controlled terminal escape sequences verbatim, just as running the selected interactive CLI directly would. Use it only with agents and workspaces you trust to control the current terminal display.
Elwood includes two local test apps.
# Terminal smoke app, Claude by default.
npm run dev:app -- --cwd /path/to/project
# Terminal smoke app with Codex.
npm run dev:app -- --agent codex --cwd /path/to/project
# Resume from Elwood metadata.
npm run dev:app -- --cwd /path/to/project --resume <elwoodSessionId>
# Browser debugger with xterm.js terminal mirror and structured event inspector.
npm run dev:web
dev:web serves http://localhost:4317. It shows the live terminal on the left
and a structured event timeline on the right. Use it to inspect hooks,
activities, warnings, startup automation, status changes, and raw event payloads.
The script is still invoked through npm, but the browser dev server process runs
under Node so node-pty can own a real interactive PTY reliably.
For the smallest real usage sample, run the minimal example. It constructs a
CodexSession, which starts Codex lazily on the first send, then makes two
ergonomic send calls — an initial prompt and a follow-up that refers back to it —
printing each assistant response, and closes the session:
npm run example:minimal
For a fuller sample with Claude/Codex selection, custom prompts, richer logging,
timeouts, and cleanup options, run examples/full.ts:
npm run example:full
npm run example:full -- --agent codex --cwd . --prompt "Summarize this repo in one paragraph."
Handlers return undefined for "no decision". Claude:
const claude = new ClaudeSession({
cwd,
autotrust: true,
hooks: {
PreToolUse(event) {
if (event.tool_name === "Bash" && event.tool_input.command.includes("rm -rf")) {
return { permissionDecision: "deny", permissionDecisionReason: "Dangerous shell command." };
}
return undefined;
},
Stop(event) {
if (event.last_assistant_message?.includes("tests are failing")) {
return { decision: "block", reason: "Tests are still failing." };
}
return undefined;
},
},
});
Codex:
const codex = new CodexSession({
cwd,
autotrust: true,
approvalPolicy: "never",
hooks: {
PermissionRequest(event) {
if (event.tool_name === "Bash") return { behavior: "allow", message: "Approved by policy." };
return { behavior: "deny", message: "Only Bash is allowed in this mode." };
},
Stop(event) {
if (event.last_assistant_message?.includes("TODO")) {
return { decision: "block", reason: "Do not stop with TODOs remaining." };
}
return undefined;
},
},
});
Hooks fail open: a handler that throws, times out, returns an invalid result,
or receives invalid input is reported as a hookError event and the agent
proceeds as if no handler ran. Empty objects are invalid Codex results and fail
open the same way.
Subscribe with session.on(eventName, handler); on returns an unsubscribe
function and may be called before the session starts.
session.on("activity", (event) => {
switch (event.kind) {
case "assistant_message":
case "reasoning":
case "tool_call":
case "tool_result":
case "web_search":
renderTimelineItem(event);
break;
}
});
| Event | Purpose |
|---|---|
activity | Adapter-neutral live stream for messages, reasoning, tools, warnings, hooks, hook results, and lifecycle. |
hook | Every parsed and validated adapter hook event. |
hook:<Name> | Hook-specific handler registration with typed response guidance. |
hookError | Handler timeout, thrown handler, invalid input, invalid response, or bridge error. Hooks fail open. |
warning | Non-fatal environment issue, such as an unparseable version, a failed autoupdate, or Claude's login_expiring notice with expiresInDays and /login (renewal and recovery). |
loop | Redacted lifecycle for persisted recurring prompts. |
terminal:data | Raw PTY output for a visual terminal renderer. |
terminal:exit | PTY process exit, emitted exactly once. |
status | Session status change. |
codex:transcript | Codex-only best-effort transcript observations not covered by hooks. |
activity events carry normalized fields for timeline rendering:
hookEventName, turnId, toolName, toolUseId, toolInput, toolOutput,
status, exitCode, failedOpen, and transcriptPath when Elwood can derive
them. raw is available for deep inspection.
Persist two things: the elwoodSessionId and the stateDir it lives in. At
boot, let Elwood decide whether to resume:
import { startOrResumeClaude } from "@with-logic/elwood";
const { session, resumed } = await startOrResumeClaude({
cwd,
stateDir,
elwoodSessionId: persisted?.elwoodSessionId,
autotrust: true,
});
if (!resumed) persist({ elwoodSessionId: session.elwoodSessionId });
startOrResumeClaude / startOrResumeCodex fall back to a fresh start only on
state_not_found, resume_unavailable (the CLI never reported a conversation
id), or adapter_mismatch, and rethrow anything else. Resume restores the
persisted launch posture (permission mode and tool rules for Claude, sandbox
and approval policy for Codex); model, hooks, and trust options are per call.
teardown() removes only Elwood-owned files: the session record, loop sidecar,
generated settings, bridge script, and socket home. It never touches the agent
CLI's own auth, transcripts, or settings. stop() is graceful; kill() is
immediate; both are safe after the process has already exited.
autotrust, Elwood answers allowlisted workspace, skill/plugin/MCP,
and Claude bypass-permissions acceptance dialogs. The library defaults to
autotrust: false; the CLI defaults to --trust. Codex hook trust is bypassed
for the entire launched session even without autotrust. Codex update dialogs
are skipped, and Claude browser-tools onboarding is dismissed. Other recognized
dialogs leave the session blocked for a human; the CLI fails such runs as
blocked_prompt. If safe trust automation cannot clear a gate within five
seconds, the library also leaves it blocked, retaining queued input. A later
supported layout can retry automatically; sendKeys permits manual recovery.--high-trust (library highTrust: true) is the agent-neutral "never ask"
switch: Claude bypassPermissions, or Codex danger-full-access with
approval policy never. It removes the agent's own guardrails, so point it
only at work you would let run unattended. Combining it with an explicit
per-agent posture is a usage error rather than a silent override.autotrust changes the security posture of the launched agent. Point it only
at directories you already trust. The standalone listClaudeModels /
listCodexModels probes always start with autotrust: true.elwoodSessionId,
adapter, cwd, and per-adapter resume state. Loop definitions are the only
persisted prompt text, in an owner-only 0600 sidecar.0600), but excluded
from session records and never read back as credentials on resume. teardown()
removes these scripts; stop() preserves them with the session's runtime files.--settings; Codex gets session-scoped -c overrides, and
Elwood restores config.toml after setModel.SubagentStop still
carries agent_transcript_path, so a host that wants those records reads the
file itself. Claude subagent transcripts are observed.listModels / setModel) is pinned to current CLI
picker layouts; drift surfaces as model_automation_failed./login recovery flow needs a human to supply the authorization code.docs/cli-behavior.md records what has been
verified against which versions.elwood command, configuration, head mode,
continuation, and trust./loop parsing.site/ is the landing page and the generated documentation site,
deployed to Vercel from this repository and served at elwood.bot. It is a static site with no framework
and no build step: HTML, CSS, ES modules, and the robot's sprite sheets.
cd site && python3 -m http.server 8766 --bind 127.0.0.1
The documentation page is generated from Markdown sources in
site/docs/guide/ and reads the CLI's help text out of
src/cli/help.ts, so the published flag reference cannot drift from the code:
cd site
uv run scripts/build_docs.py # regenerate guide/, llms.txt, llms-full.txt
python3 scripts/check_doc_examples.py # typecheck every documented example
node --test tests/*.test.mjs # the landing page's own tests
MIT. See LICENSE.
Setup, the merge gate, the e2e suite, the dev apps, and the repository map are in CONTRIBUTING.md. Report vulnerabilities privately as described in SECURITY.md.
ClaudeSession.login() drives /login with a human-supplied code and
validates the URL and code it handles, but should only run at a genuine login
prompt. See docs/login-recovery.md.