Vectrum
Sign In
128 lines · 8.0 KB

Using it in place of claude

A lot of CI jobs and shell scripts run an agent as claude -p. Codey reads the same flags with the same meaning, so pointing one of them at Codey is meant to be a matter of changing the binary's name:

codey -p --output-format json "why does TestFoo fail?" | jq -r .result
echo "summarise the last commit" | codey -p --allowedTools "Bash(git log:*)" "Bash(git show:*)"
codey -c -p "now fix it"                                   # same session
Claude Code flagIn Codey
-p, --printOne prompt, then exit. The prompt may follow -p — Codey's own spelling — or be a positional argument anywhere on the line, as claude takes it, or arrive on stdin when there is no other
--output-format textThe final answer and nothing else on stdout
--output-format jsonOne result object at the end
--output-format stream-jsonOne JSON line per message as the run goes, then the result object
--verboseThe same as -v. With json it prints every line stream-json would have, as one array, which is what Claude Code does; stream-json does not need it
-c, --continue-resume latest: the most recent session in this working root
--resume <id>Unchanged; the id is a Codey session id, which is what session_id in the output holds
--model, --max-turnsCodey's own flags, already spelled the same
--allowedTools, --allowed-toolsCalls to approve without asking, as rules
--disallowedTools, --disallowed-toolsTools to take away, or calls to refuse, as rules
--permission-modedefault; acceptEdits, which lets edits and writes through and still asks before a command — the mode the ACP front end has by the same name; bypassPermissions, which is -yolo; dontAsk, which refuses whatever no rule allows
--dangerously-skip-permissions-yolo
--append-system-prompt, --system-promptAdd to the end of Codey's system prompt, or replace it. The session's own agent only: a subagent's prompt is its preset's
--include-partial-messagesAccepted, with a warning, and ignored

Without --output-format, -p prints what it always has: the transcript as it streams, the tool calls, a usage line — for a person watching, not a parser. Asking for a format is asking for Claude Code's behaviour, and then stdout holds the format and nothing else; notices, warnings and any approval prompt go to stderr.

The shapes are Claude Code's, because the programs reading them were written against it. The result object carries type, subtype (success, error_max_turns or error_during_execution), is_error, result, session_id, num_turns, duration_ms, duration_api_ms, total_cost_usd and usage with the four token counts. In stream-json it is preceded by a system/init line — working directory, model, tools, MCP servers, permission mode — then an assistant line per response, holding the message as the Messages API returned it, and a user line per batch of tool results. Numbers are this invocation's: a resumed session reports what the resume cost, not everything before it.

A run that fails exits 1, whatever the format, and the JSON formats still print their result object — with is_error: true, the error in errors, and in result whatever the model last said, or the error itself when it said nothing. That includes failing before the model was ever asked: no credentials gives a result object, with an empty session_id because there was no session yet. Check is_error rather than subtype: it is the field that means failure in both, where the subtypes for a failure are not guaranteed to agree between the two. A flag Codey does not know stops the run before any of this, with exit 2, and so does a value it cannot use — --permission-mode plan, say — with exit 1 and a message on stderr.

Who gets asked. Claude Code never prompts in -p mode, and with --output-format neither does Codey when stdin is not a terminal: what no rule allows is refused, and the model is told so and carries on. That is the case in CI. At a terminal the prompt still comes up, on stderr. Codey's own -p without a format keeps reading answers from stdin, so yes | codey -p ... works as it always has — except when the prompt itself came from stdin, which leaves nothing there to answer with.

Rules. --allowedTools and --disallowedTools take Claude Code's rule syntax, comma- or space-separated, as many as you like after one flag: Bash, Bash(git status), Bash(npm run test:*), Read, Read(./src/**), Edit(./docs/**), Write, Glob, Grep, WebFetch(domain:example.com), WebSearch, TodoWrite, Task, mcp__github, mcp__github__create_issue. Claude Code's names map onto Codey's tools — Bash is bash, Read read_file, Write write_file, and Edit both edit_file and write_file, because Claude Code applies its Edit rules to every tool that edits files. Codey's own names work as well. A rule naming a tool Codey does not have, NotebookEdit say, is a warning, and the rest of the list still applies.

A rule for a whole tool on the disallowed list takes the tool away: the model is never shown it. A narrower one — Bash(git push:*), Read(./.env) — is checked at approval time, before anything else, -yolo included, and a call it matches is refused. On the allowed list a rule stands in for the "yes" you would otherwise have been asked for.

Bash(prefix:*) matches word by word, so Bash(ls:*) covers ls -la and not lsof. A compound command is allowed only when every simple command in it is covered, so git status; rm -rf ~ does not ride in on Bash(git status:*), and anything the shell analysis cannot see through — command substitution, a variable in command position, an inline bash -c script, an assignment like PATH=..., output redirected into a file — is never allowed by a narrower rule than Bash. A deny rule is the other way round: one matching part is enough, and it looks inside bash -c and eval, past sudo, env and timeout, and at /bin/rm as rm. That catches the command a model would ordinarily write. It is not a sandbox, for the reason the Safety section gives.

Path rules read like gitignore, as Claude Code's do: ./ and / are the working root, // an absolute path, ~/ your home, and a pattern with no slash in it — .env, *.pem — matches a name at any depth. Path rules on Read apply to glob and grep too, as far as their path argument goes.

What does not work:

  • --permission-mode plan. Codey has no read-only planning mode, and says so rather than run a mode it does not have. Ask for a plan in the prompt, and --disallowedTools Edit,Write,Bash holds the run to it.
  • --input-format stream-json, and so a conversation fed in over stdin. The prompt is text.
  • --include-partial-messages: there are no stream_event lines, only whole messages.
  • --mcp-config and --strict-mcp-config. MCP servers come from Codey's config.
  • --settings, --setting-sources, --add-dir, --fallback-model, --session-id, --fork-session, and --resume with no id, which in Claude Code opens a picker.
  • Piping a file in alongside a prompt. cat log | claude -p "explain" hands Claude Code both; Codey reads stdin only when there is no prompt otherwise, because a CI runner can leave stdin as a pipe nobody ever closes, and a run that waited on it would hang there for good. Put it in the prompt: codey -p "explain: $(cat log)".
  • A prompt without -p, which in Claude Code starts an interactive session with that prompt. Codey ignores it and says so.
  • Tool names in the output are Codey's — bash, read_file — not Claude Code's. A script that looks for "name":"Bash" in the stream needs to look for bash.
  • The result object has no permission_denials or modelUsage, and lines carry no parent_tool_use_id other than null: subagents report through a tool result rather than in the parent's stream.