effgen code · 1.0.0

A coding agent that shows you every change as a diff before it touches disk

effgen code runs a plan, act and observe loop over one directory: it proposes an approach, writes files, executes code in a sandbox, reads the real output and iterates. Every write and every command passes a permission gate you choose, every applied edit can be reversed, and the files it edits stay inside the workspace root.

4
Permission modes
26
Slash commands
100
Edits you can undo
28
Git commands refused
The loop

One task, start to finish

This is a recording of a real run against a four-file Python package with an AGENTS.md, on a clean git tree. It is played back a line at a time; the whole transcript is in the page from the first frame, so it can be selected, copied and read at any point.

effgen code99/99Replay finished.
$ effgen code "Add title_case(text) to textkit/case.py: title-case each word, raise TypeError on a non-string. Re-export it from textkit/__init__.py. Add tests/test_case.py covering both cases. Then run pytest and tell me the result." -y -m openai:gpt-5.4
Workspace: /data/wang/gks/effgen-demo/textkit
Repo: textkit · branch main · clean tree · 4 file(s)
Project instructions: /data/wang/gks/effgen-demo/textkit/AGENTS.md
Permissions: yes — apply writes, runs and shell commands without asking
2026-08-20 12:53:08,563 - effgen.security.sandbox - WARNING - 
┌─────────────────────────────────────────────────────────────┐
│  effGen SANDBOX WARNING                                      │
│                                                              │
│  Code execution is using SubprocessSandbox, which provides   │
│  PARTIAL isolation only.                                     │
│                                                              │
│  Limitations:                                                │
│  • Executed code can READ any host file the calling          │
│    process's user can read — reads are NOT confined          │
│  • Writes are confined to the run's working directory only   │
│    where the kernel allows it; each result reports what was  │
│    enforced in 'filesystem_confined' / 'writable_root'       │
│  • Network isolation via unshare (may require privileges)    │
│  • Memory limit is advisory, not hard-enforced               │
│                                                              │
│  To confine reads as well, install Docker and ensure the     │
│  daemon is running and accessible by the current user.       │
└─────────────────────────────────────────────────────────────┘

new file textkit/case.py (+7/-0)
--- a/textkit/case.py
+++ b/textkit/case.py
@@ -0,0 +1,7 @@
+"""Case conversion helpers."""
+
+
+def title_case(text: str) -> str:
+    if not isinstance(text, str):
+        raise TypeError("text must be a string")
+    return " ".join(word.capitalize() for word in text.split())
  ✓ Write textkit/case.py (+7/-0)
edit textkit/__init__.py (+2/-1)
--- a/textkit/__init__.py
+++ b/textkit/__init__.py
@@ -1,5 +1,6 @@
 """Small text helpers."""
 
+from .case import title_case
 from .wrap import wrap_words
 
-__all__ = ["wrap_words"]
+__all__ = ["wrap_words", "title_case"]
  ✓ Write textkit/__init__.py (+2/-1)
new file tests/test_case.py (+16/-0)
--- a/tests/test_case.py
+++ b/tests/test_case.py
@@ -0,0 +1,16 @@
+from textkit import title_case
+
+
+
+def test_title_cases_each_word():
+    assert title_case("hello world from textkit") == "Hello World From Textkit"
+
+
+
+def test_title_case_rejects_non_string():
+    try:
+        title_case(123)
+    except TypeError:
+        pass
+    else:
+        raise AssertionError("expected TypeError")
  ✓ Write tests/test_case.py (+16/-0)
  ✓ Run python: import subprocess, sys result = subprocess.run([sys.executable, '-m', 
'pytest',…
╭──────────────────────────────────── Coding Agent ────────────────────────────────────╮
│ I added title_case and the requested tests, then ran pytest.                         │
│                                                                                      │
│ Files changed:                                                                       │
│                                                                                      │
│  • textkit/case.py                                                                   │
│  • textkit/__init__.py                                                               │
│  • tests/test_case.py                                                                │
│                                                                                      │
│ Implemented:                                                                         │
│                                                                                      │
│  • title_case(text) title-cases each whitespace-separated word                       │
│  • Raises TypeError if text is not a string                                          │
│  • Re-exported from textkit.__init__                                                 │
│                                                                                      │
│ Pytest result:                                                                       │
│                                                                                      │
│  • The test run did not complete successfully                                        │
│  • Actual execution result: EXIT_CODE=-9                                             │
│                                                                                      │
│ That means the pytest process was killed by the environment before producing test    │
│ output. So I can’t honestly report passing/failing tests from this run, only that    │
│ the run was terminated.                                                              │
╰──────────────────────────────────────────────────────────────────────────────────────╯
✓ Done in 47.9s · 8 tools · 8,136 tokens · $0.0275
Tool calling: hybrid — provider tool API first, falling back to the text
Files written in /data/wang/gks/effgen-demo/textkit: textkit/case.py, 
textkit/__init__.py, tests/test_case.py
Three files written, each shown as a unified diff first, and the tests run in the sandbox. The agent reports that the test process was killed by the sandbox’s memory limit on this machine rather than claiming a pass — run outside the sandbox, the three tests pass. Recorded against effGen 1.0.0.$ effgen code "Add title_case(text) to textkit/case.py: title-case each word, raise TypeError on a non-string. Re-export it from textkit/__init__.py. Add tests/test_case.py covering both cases. Then run pytest and tell me the result." -y -m openai:gpt-5.4
01

Read

Before the first model call the agent builds an inventory of the workspace: the branch, a short git status and a bounded file layout with ignored files excluded, plus an AGENTS.md if the workspace has one. Outside a repository the same inventory comes from the directory itself.

02

Plan

The model proposes an approach and the files it wants to touch. In plan mode that is where the run stops: the diffs are rendered and nothing is written.

03

Diff

Every proposed edit is rendered as a unified diff before the write is decided — in every mode, including plan mode and including piped output, where the diff goes to stderr so stdout keeps carrying only the result.

04

Apply

The permission mode decides what happens next: propose, confirm each, apply writes but confirm shell commands, or apply everything. Each applied edit is journaled per workspace so it can be reversed.

05

Run

Code runs in the sandbox effGen already ships — Docker when its daemon is reachable, otherwise a subprocess sandbox that isolates the network and confines writes to the workspace. The run reports which of those it actually enforced rather than assuming.

06

Fix

The agent reads the real output — not a prediction of it — and iterates until the task is done or the iteration cap is reached. A run that spends every step without an answer says so, and reports what it reached as partial progress.

Permission modes

You decide what it may do

Pick at most one. Naming two is an error rather than a silent precedence rule. Without a terminal there is nobody to confirm, so the default becomes plan mode and the run writes nothing unless you opt in.

What each permission mode allows, for file writes, sandboxed runs, shell commands and commits
ModeWritesSandboxed runsShell commandsCommit
--planPropose only. Show the diffs, write nothing, run nothing.proposed onlynonono
(default)With a terminal, show each diff and confirm every write and command.confirm eachconfirm eachconfirm eachconfirm
--auto-editApply writes and sandboxed runs. Shell commands still need confirming.appliedappliedconfirm eachconfirm
--yesApply writes, sandboxed runs and shell commands without asking.appliedappliedappliedapplied

Plan mode, on the same package

The diff is rendered, the write is withheld with the reason, and the footer counts what was proposed. Nothing on disk changed: the directory held the same four files before and after.

effgen code --plan
$ effgen code "Add a strip_accents(text) helper to textkit/accents.py and re-export it from the package." --plan -m openai:gpt-5.4Workspace: /data/wang/gks/effgen-demo/textkitRepo: textkit · branch main · clean tree · 4 file(s)Project instructions: /data/wang/gks/effgen-demo/textkit/AGENTS.mdPermissions: plan — propose only; no file is written and no command runsnew file textkit/accents.py (+10/-0)--- a/textkit/accents.py+++ b/textkit/accents.py@@ -0,0 +1,10 @@+"""Helpers for removing accents from text."""++import unicodedata+++def strip_accents(text: str) -> str:+    if not isinstance(text, str):+        raise TypeError("text must be a string")+    normalized = unicodedata.normalize("NFKD", text)+    return "".join(char for char in normalized if not unicodedata.combining(char))  ⚠ Write textkit/accents.py (+10/-0) — withheld: plan mode does not write files; re-runwith --auto-edit (or --yes) to allow it.╭──────────────────────────────────── Coding Agent ────────────────────────────────────╮│ I couldn’t apply the requested code changes because the file tool is currently in    ││ plan mode and refused writes:                                                        ││                                                                                      ││  • plan mode does not write files; re-run with --auto-edit (or --yes) to allow it    ││                                                                                      ││ Because of that restriction, I was not able to create textkit/accents.py, update     ││ textkit/__init__.py, add tests, or run them against actual changes.                  ││                                                                                      ││ If you re-run with write approval enabled, I would make these changes:               ││                                                                                      ││  • add textkit/accents.py with strip_accents(text)                                   ││  • re-export it from textkit/__init__.py                                             ││  • add tests/test_accents.py                                                         ││  • run pytest and report the real results                                            │╰──────────────────────────────────────────────────────────────────────────────────────╯✓ Done in 21.6s · 8 tools · 8,031 tokens · $0.0320Tool calling: hybrid — provider tool API first, falling back to the textPlan mode: 1 action(s) (write) were proposed and not carried out. Re-run with --auto-edit (or --yes) to apply them.

Naming two modes stops the run

Rather than picking one for you, the command says which flags conflict and exits. The same holds for --review, which cannot be combined with a permission flag, --commit or --undo — each asks for something a read-only run will not do.

conflicting flags
$ effgen code -p "anything" --plan --auto-edit✗ --plan, --auto-edit cannot be combined — they select different permission modes. Pass one.

The environment the run reads

VariableTypeDescription
EFFGEN_WORKSPACEThe directory the agent reads and writes, and the only one sandboxed code may write to (created if missing). Unset: the current directory. -w/--workspace overrides it.
EFFGEN_SANDBOX_BACKENDdocker or subprocess. Docker confines the filesystem and network for executed code; the subprocess fallback isolates the network and confines writes to the workspace, leaving the rest of the filesystem readable but read-only.
Diffs and undo

Every applied edit is reversible

Applied edits are journaled per workspace — the last 100 of them — so a change can be reversed after the session has ended and after the shell has closed. A restored file returns to its previous content; a file the run created is removed.

effgen code --undo
$ effgen code --undoRestored textkit/__init__.py to its previous content.5 earlier change(s) can still be undone.

The journal is per workspace and lives on disk, so it survives a restart — there is nothing about it for a session to restore. --undo-count N reverses several, and /undo [n] does the same inside a session.

Staging edits without writing them

In a session, /plan stages edits without writing them, /diff shows what is staged, /apply [n] writes them — all of them, or one by number — and /reject [n] discards them.

When a hunk no longer applies

A staged edit is prepared against the file as it was. If the file changed underneath, the hunks that still match are applied and the rest are reported by name. The file is never overwritten with a stale version, so an edit you made outside the session is not silently lost.

Review

A review holds no tool that writes

--review is not a permission mode with a different prompt. The run holds no tool that writes a file, runs code or runs a shell command: the file tool is narrowed to its reading operations and its schema carries no write, and git is the read-only surface, pinned to the workspace. The permission gate still stands behind them, so a review is read-only by the tools it holds and by the mode it runs in.

effgen code --review
$ effgen code --review -p "is this change consistent with AGENTS.md?" -m openai:gpt-5-nanoWorkspace: /data/wang/gks/effgen-demo/textkitRepo: textkit · branch main · 2 change(s) · 5 file(s)Project instructions: /data/wang/gks/effgen-demo/textkit/AGENTS.mdReviewing uncommitted changes, 15 line(s)Read-only review: no file is written and no command runs.2026-08-20 12:39:52,875 - effgen.models.openai_adapter - WARNING - Model 'gpt-5-nano' returned no visible text: it spent the whole output budget on internal reasoning and hit a max_tokens cap of 4096 (finish_reason='length'). It produced 4096 reasoning tokens and no answer. Raise max_tokens — e.g. agent.run(task, max_tokens=8192) — or use a model that answers without an extended reasoning chain.╭─────────────────────────────────────── Review ───────────────────────────────────────╮│                                                                                      ││  • textkit/init.py (line containing the new import and the updated all): The change  ││    adds a public export by importing title_case from .case and updates all to        ││    include "title_case" alongside "wrap_words". This aligns with the project         ││    guidance that public helpers are re-exported from init.py. However, exact line    ││    numbers aren’t visible in the excerpt provided, so I can’t cite a precise line    ││    number. [1]                                                                       ││  • textkit/case.py (title_case implementation): The file defines title_case(text:    ││    str) and enforces the string type with a TypeError for non-strings, in line with  ││    the stated API contract. The implementation uses a regex to capitalize each       ││    non-whitespace sequence, preserving whitespace, which matches the described       ││    behavior in the docstring. This appears correct and self-consistent with the      ││    intended public API. [2]                                                          ││  • Additional observations:                                                          ││  • Circular import risk: The new import in init.py (from .case import title_case)    ││    does not appear to create a circular import, since case.py only imports the re    ││    module and does not rely on textkit.init at runtime.                              ││  • Behavior details: title_case capitalizes the first character of each              ││    non-whitespace segment and lowercases the rest. This is a reasonable              ││    interpretation of “title case” for many cases, but note that hyphenated words or  ││    words with internal punctuation will be treated as a single segment (e.g.,        ││    "this-is" -> "This-is", not "This-Is"). This is consistent with the current       ││    implementation, but it’s worth documenting or adjusting if a different behavior   ││    is desired.                                                                       ││  • Test coverage: The repository listing shows tests/test_wrap.py, but there’s no    ││    visible test for title_case in the provided excerpts. Adding tests for title_case ││    would help verify edge cases (spaces, multiple spaces, empty strings,             ││    single-character words, digits, punctuation, etc.).                               ││  • Typing in case.py: The annotation cap_word(m: re.Match) -> str uses re.Match.     ││    This is generally fine in Python 3.11+ for runtime annotations, but if future     ││    compatibility or mypy checks are a concern, one might consider from typing import ││    Match and use Match[str] for clarity.                                             ││                                                                                      ││ What I would change (described, not implemented):                                    ││                                                                                      ││  • Add a dedicated test suite for title_case, e.g., tests/test_title_case.py, to     ││    cover:                                                                            ││  • Basic cases: "hello world" -> "Hello World"                                       ││  • Already-title-cased input: "Hello" remains "Hello"                                ││  • Mixed case with punctuation/spaces: "foo BAR baz" -> "Foo Bar Baz"                ││  • Empty string and string with only spaces                                          ││  • Hyphenated words: "this-is a test" vs the chosen convention                       ││  • If desired behavior for hyphenated words or internal punctuation should differ,   ││    adjust title_case (textkit/case.py) accordingly and add tests for those cases.    ││  • Optionally add a short docstring note in textkit/init.py explaining that          ││    title_case is re-exported for public API completeness.                            ││                                                                                      ││ Reasoning references to the changed parts:                                           ││                                                                                      ││  • The new import and all update are visible in the provided init.py excerpt. [1]    ││  • The title_case function and its type-checking, docstring, and implementation      ││    details are visible in the provided case.py excerpt. [2]                          │╰──────────────────────────────────────────────────────────────────────────────────────╯✓ Done in 108.5s · 2 tools · 19,916 tokens · $0.0062Tool calling: hybrid — provider tool API first, falling back to the text

The change under review is handed to the model as context, because the only route to a diff would be a shell and a read-only run has none. A diff over the budget is truncated with the cut marked and the remainder counted, never silently. The record reports "read_only": true and a review block naming the target, and files_written is always empty.

What you can point it at

TargetTypeDescription
uncommittedthe default — git diff HEAD, staged and unstaged together
stagedgit diff --cached
HEAD~3any revision git accepts
main...HEADany range git accepts
-f PATHone file in full, repeatable — works with a target or on its own, which is how a directory that is not a repository is reviewed

effgen code --review [TARGET] · -f/--file is repeatable

And when there is nothing to review

Outside a repository with no -f, the run exits 1 naming the three ways to give it a subject rather than reviewing something else. On a clean tree it says so instead of returning an empty review.

not a repository
$ effgen code --review -p "review this"   # outside a repository✗ /data/wang/gks/effgen-demo/notarepo is not inside a git repository, so there is no diff to review. Name the files instead (-f/--file PATH, repeatable), or run the review from inside a repository.
clean tree
$ effgen code --review✗ There is nothing to review in /data/wang/gks/effgen-demo/textkit: everything is committed. Make or stage a change, pass a revision (--review HEAD~1), or name files with-f/--file.
The interactive session

26 slash commands, and a session you can leave

On a terminal with no task, effgen code opens a session. Type / on its own for the menu; tab completes command names. effgen chat has its own 13.

effgen code
$ effgen code -m gemini:gemini-3.1-flash-lite effGen v1.0.0 · codeModel: gemini-3.1-flash-liteWorkspace: /data/wang/gks/effgen-demo/textkitRepo: textkit · branch main · clean tree · 4 file(s)Project instructions: /data/wang/gks/effgen-demo/textkit/AGENTS.mdPermissions: ask — confirm every write and every commandDescribe a change and press Enter.  End a line with \ for multi-line.Slash commands (type / for the menu): /help  /plan  /review  /run  /test  /diff  /apply /undo  /context  /add  /model  /git  /exitcode · gemini-3.1-flash-lite · ask › /help                                    Coding commands                                     ┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┃ Command  ┃ Does                                                                      ┃┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩│ /help    │ Show this help (or type just / for the menu)                              ││ /plan    │ Propose a change without writing it:  /plan add retries to fetch.py       ││ /review  │ Review, read-only, without writing or running anything:  /review          ││          │ [uncommitted|staged|<rev>]                                                ││ /run     │ Run a shell command in the workspace:  /run ls -la                        ││ /test    │ Run tests in the workspace:  /test [pytest args]                          ││ /diff    │ Show the edits staged by the last /plan (or the last turn's edits)        ││ /apply   │ Apply the staged edits:  /apply [n]  (n = one edit by number)             ││ /reject  │ Discard the staged edits:  /reject [n]                                    ││ /undo    │ Reverse the last applied edit(s):  /undo [n]                              ││ /context │ Show the files in context and their token/size estimate (/context refresh ││          │ re-reads the project layout)                                              ││ /add     │ Add a workspace file to context:  /add src/app.py                         ││ /drop    │ Remove a file from context:  /drop src/app.py                             ││ /mode    │ Show or set the permission mode:  /mode ask|auto-edit|yes|plan            ││ /model   │ Hot-swap the active model:  /model gpt-5-nano                             ││ /tools   │ List the coding tools the agent can call                                  ││ /cost    │ Session token + cost total                                                ││ /trace   │ Show the last turn's reasoning/tool steps                                 ││ /git     │ Git for the workspace:  /git [status|diff|log|branch|show|remote] · /git  ││          │ staged (pull the staged diff into context) · /git commit [message]        ││ /reset   │ Clear the conversation memory (keep files-in-context)                     ││ /clear   │ Reset live context (memory + files) but keep the session on disk          ││ /compact │ Summarize the conversation so far to extend a long session                ││ /save    │ Save this coding session to a file:  /save [name]                         ││ /session │ Show, name or resume the session id:  /session [id]  (the same store as   ││          │ `effgen code --session-id`)                                               ││ /load    │ Load a saved coding session:  /load [name|number]                         ││ /doctor  │ Run a quick environment check                                             ││ /exit    │ Leave the coding agent (also: /quit, exit, quit)                          │└──────────┴───────────────────────────────────────────────────────────────────────────┘code · gemini-3.1-flash-lite · ask › /modePermission mode: ask — confirm every write and every commandChange with:  /mode ask | auto-edit | yes | plancode · gemini-3.1-flash-lite · ask › /context Files in contextWorkspace: /data/wang/gks/effgen-demo/textkitRepo: textkit · branch main · clean tree · 4 file(s)Project instructions: /data/wang/gks/effgen-demo/textkit/AGENTS.mdProject layout in the prompt: 4 file(s), 4 line(s) — /context refresh to re-readFiles pinned with /add: (none) — add one with /add <file>code · gemini-3.1-flash-lite · ask › /git Git — status## main  'staged' pulls the staged diff into the conversation; 'commit' records this session's edits after confirming.code · gemini-3.1-flash-lite · ask › /cost Session costTurns: 0Tokens: 0Cost: $0.00code · gemini-3.1-flash-lite · ask › /exitGoodbye! 💡 Tip: effgen code writes and runs code in your workspace, showing each edit as a diff.
Every slash command a coding session accepts, and what each does
CommandDoes
/helpShow this help (or type just / for the menu)
/planPropose a change without writing it: /plan add retries to fetch.py
/reviewReview, read-only, without writing or running anything: /review [uncommitted|staged|<rev>]
/runRun a shell command in the workspace: /run ls -la
/testRun tests in the workspace: /test [pytest args]
/diffShow the edits staged by the last /plan (or the last turn's edits)
/applyApply the staged edits: /apply [n] (n = one edit by number)
/rejectDiscard the staged edits: /reject [n]
/undoReverse the last applied edit(s): /undo [n]
/contextShow the files in context and their token/size estimate (/context refresh re-reads the project layout)
/addAdd a workspace file to context: /add src/app.py
/dropRemove a file from context: /drop src/app.py
/modeShow or set the permission mode: /mode ask|auto-edit|yes|plan
/modelHot-swap the active model: /model gpt-5-nano
/toolsList the coding tools the agent can call
/costSession token + cost total
/traceShow the last turn's reasoning/tool steps
/gitGit for the workspace: /git [status|diff|log|branch|show|remote] · /git staged (pull the staged diff into context) · /git commit [message]
/resetClear the conversation memory (keep files-in-context)
/clearReset live context (memory + files) but keep the session on disk
/compactSummarize the conversation so far to extend a long session
/saveSave this coding session to a file: /save [name]
/sessionShow, name or resume the session id: /session [id] (the same store as `effgen code --session-id`)
/loadLoad a saved coding session: /load [name|number]
/doctorRun a quick environment check
/exitLeave the coding agent (also: /quit, exit, quit)

Read from the session’s own command table in effGen 1.0.0.

What a turn shows while it runs

An interactive turn shows a status line naming the tool in flight, each proposed edit’s diff before it is written, and a tick per decided action. On a provider that streams its tool calls the answer is written to the screen as the model produces it, rather than arriving in one block when the turn ends. The status line and the answer take turns owning the terminal: the status line runs while the model is thinking and dispatching tools, and hands over from the first word of the answer.

And where it does not stream

A model whose tool calls are not streamed — the local engines among them — prints its answer once the turn finishes, and so does every non-interactive surface: piped output, --json, -q, --no-animation and NO_COLOR. Those three also render plain text with no escape codes, which is what makes a captured session readable as text rather than as a screenful of control characters.

Continuing a session

--session-id ID (or --resume ID) continues a stored session — the same store as effgen chat --session-id and effgen sessions list. What it restores, and what it deliberately does not:

  • The conversation — restoredrestored — the next turn can answer from what earlier turns said
  • Files in context — restoredrestored, minus any path that no longer exists, which is named
  • Files the session wrote — restoredrestored, so a commit still knows its own paths
  • Model and provider — restoredrestored when -m was not given, and announced
  • Permission mode — restoredrestored only on a terminal and only when no permission flag was given — a stored yes is never restored into a piped run
  • The workspace — not restorednot adopted: -w/--workspace or the current directory always wins, and a stored workspace that differs is reported in one line
  • Edits staged by /plan — not restorednot stored; the files may have changed underneath, so re-run /plan
  • The undo journal — restorednothing to restore — it is per workspace and already on disk, so --undo works across restarts
Repository awareness

It reads your repository. It will not rewrite it

In a git repository the branch, a short status and a bounded file layout (ignored files excluded) are read before the first model call and become part of the agent’s context, along with an AGENTS.md in the workspace if there is one. The single repository change a session can make is a commit of the files it wrote, after an explicit confirmation.

the git allow-list
$ python -c 'from effgen.cli.code.git_actions import unsafe_shell_git; …'allowed:   add, commit, config, diff, ls-files, rev-parse, statusrefused:   am, apply, branch, checkout, cherry-pick, clean, fetch, filter-branch, gc, merge, mv, notes, prune, pull, push, rebase, reflog, remote, reset, restore, revert, rm, stash, submodule, switch, tag, update-ref, worktree $ git status  -> allowed$ git commit -m 'wip'  -> allowed$ git push origin main  -> git push is not available to a coding session: it publishes, rewrites or discards work.$ bash -c 'git reset --hard HEAD~1'  -> git reset is not available to a coding session: it publishes, rewrites or discards work.$ git commit --amend  -> git commit --amend is not available to a coding session: it would rewrite, force or discard work.$ python3 -c "import subprocess; subprocess.run(['git', 'push'])"  -> git push is not available to a coding session: it publishes, rewrites or discards work.

The refusal is not a prompt-level instruction. Every git command a session can reach passes the same check, and it reads the command line the model asked for — so git push is refused whether it is asked for directly, wrapped in bash -c, or spawned from a Python one-liner.

It may run

  • git add
  • git commit
  • git config
  • git diff
  • git ls-files
  • git rev-parse
  • git status

The commit is limited to the paths the run wrote, so work you had staged for other files stays staged and out of it. The plan — repository, exact paths, message — is printed before the confirmation.

It refuses, in every mode

  • am
  • apply
  • branch
  • checkout
  • cherry-pick
  • clean
  • fetch
  • filter-branch
  • gc
  • merge
  • mv
  • notes
  • prune
  • pull
  • push
  • rebase
  • reflog
  • remote
  • reset
  • restore
  • revert
  • rm
  • stash
  • submodule
  • switch
  • tag
  • update-ref
  • worktree

Along with 10 flags wherever they appear: --amend, --delete, --force, --force-with-lease, --hard, --keep, --mixed, --no-verify, -D, -f.

Scripting

One JSON document on stdout, everything else on stderr

Piped or with --json, stdout carries only the result — the answer text, or one JSON document — and everything a human reads goes to stderr. That is what makes the command safe to put in a pipeline.

bash
# the task as an argument, the result as one JSON document
effgen code -p "what does this package export?" --plan --json \
  -m gemini:gemini-3.1-flash-lite < /dev/null | jq -r .permission_mode

# a failing log becomes context in front of the task
cat pytest.log | effgen code -p "why did this fail?" --plan --json \
  -m gemini:gemini-3.1-flash-lite | jq -r .tool_calling

# with no task at all, the piped text is the task
echo "what does textkit/wrap.py do?" | effgen code --plan --json \
  -m gemini:gemini-3.1-flash-lite | jq -r .task

# nothing to pipe in? say so, or the read to EOF holds the run
effgen code -p "write fib.py with fib(n) and print fib(10)" -w ../ws \
  --auto-edit --json -m openai:gpt-5-mini < /dev/null | jq -r ".files_written[]"
what the four commands printed, in order
plan
hybrid
what does textkit/wrap.py do?
fib.py

When stdin is not a terminal it is read to EOF before the run starts, which is what lets a producer that writes slowly — a build log, tail -f — be folded in whole. The consequence is that a pipe which never closes holds the run, so after about two seconds the command prints a line to stderr naming what it is waiting for and how to skip it.

What the document carries

The 26 keys below are the top level of the document effgen code -p "list the public helpers this package exports" --plan --json printed. Every proposed edit appears in diffs; the ones that reached disk carry "applied": true, so --plan --json reports the changes it would make without writing any of them.

  • actions
  • answer
  • answer_source
  • coding_suitability
  • commit
  • cost_usd
  • diffs
  • duration_s
  • error
  • files_written
  • iterations
  • model
  • partial_output
  • permission_mode
  • provider
  • read_only
  • reason
  • repo
  • review
  • success
  • task
  • tokens
  • tool_calling
  • tool_calls
  • withheld
  • workspace
tool_calling
Names the path the model’s tool calls travelled on. hybrid and native send the definitions to the provider’s tool-calling API; react reads the calls out of the model’s text.
answer_source
Names where the answer came from when the loop had to recover one, and is empty when the model wrote it.
coding_suitability
Carries the verdict on the chosen model — suitable, limited, unsuitable, or unknown when the catalog does not know the id.
actions
The full log of what was allowed, withheld, declined or refused, and why.
When it does not work

A run that did not do the work says so

A coding agent that reports success for a turn in which nothing was written is worse than one that fails, because the failure is invisible until someone reads the diff. These are the states that get their own report rather than being rounded up to an answer.

The model describes a call instead of making one

Some small models answer with the tool call written out as text. Nothing was written and nothing ran, so the turn is reported as a failure naming the tool whose call was written out — not as an answer describing work that did not happen. An answer that recaps a call the run really made keeps its result.

written_tool_call

The run stops at its iteration cap

A run that spends every step without writing a final answer has no answer to report. The run states what stopped it and what to do about it, and whatever it had reached is reported separately as partial progress — tool output and reasoning, never presented as a result.

max_iterations_partial · max_iterations_exhausted

The answer is not what the model wrote

A model that keeps repeating the same call, or returns no final answer, leaves the loop with nothing to hand back but the last tool result or its own recovered text. The run completed, so the footer says so — and the line under it names where the answer came from instead of passing a tool result off as an answer.

loop_detected · repeated_tool_result · null_final_from_model

A hunk no longer applies

When a file changed underneath a staged edit, the hunks that still match are applied and the rest are reported by name. The file is not overwritten with a stale version, so work done outside the session is not lost.

There was nobody to confirm

Without a terminal there is nobody to answer a prompt, so the default becomes plan mode: the run reports what it would do and writes nothing. A run that completed but withheld its changes for that reason exits 2, which includes a commit that could not be confirmed.

A failed action stays a failed action

In this recording the agent tried to run the test suite twice and both attempts failed — once because the sandbox killed the process, once because the code it wrote was not valid on its own. Both are marked as failures in the action list, and the footer names where the answer came from because the model never wrote one.

a run with failed actions
$ effgen code "Add title_case(text) to textkit/case.py … Run pytest and report the result." -y -m openai:gpt-5-miniWorkspace: /data/wang/gks/effgen-demo/textkitRepo: textkit · branch main · clean tree · 4 file(s)Project instructions: /data/wang/gks/effgen-demo/textkit/AGENTS.mdPermissions: yes — apply writes, runs and shell commands without asking2026-08-20 12:50:02,222 - effgen.security.sandbox - WARNING - ┌─────────────────────────────────────────────────────────────┐│  effGen SANDBOX WARNING                                      ││                                                              ││  Code execution is using SubprocessSandbox, which provides   ││  PARTIAL isolation only.                                     ││                                                              ││  Limitations:                                                ││  • Executed code can READ any host file the calling          ││    process's user can read — reads are NOT confined          ││  • Writes are confined to the run's working directory only   ││    where the kernel allows it; each result reports what was  ││    enforced in 'filesystem_confined' / 'writable_root'       ││  • Network isolation via unshare (may require privileges)    ││  • Memory limit is advisory, not hard-enforced               ││                                                              ││  To confine reads as well, install Docker and ensure the     ││  daemon is running and accessible by the current user.       │└─────────────────────────────────────────────────────────────┘ new file textkit/case.py (+12/-0)--- a/textkit/case.py+++ b/textkit/case.py@@ -0,0 +1,12 @@+"""Title-case each word."""+++def title_case(text: str) -> str:+    """Return `text` with each word title-cased.++    Raises TypeError if text is not a string.+    """+    if not isinstance(text, str):+        raise TypeError("text must be a string")+    # Split on whitespace and capitalize each word (first char uppercase, rest lowercase)+    return " ".join(word.capitalize() for word in text.split())  ✓ Write textkit/case.py (+12/-0)  ✗ Run bash: # Running pytest after adding files pytest -q — failed: Code exited with status 137: bash: line 2: 142 Killed pytest -q2026-08-20 12:51:01,716 - effgen.tools.base_tool - WARNING - Tool 'python_repl': ignoring unknown parameters {'language'}  ✗ Run python: # Running pytest after adding files pytest -q — failed: NameError: name 'pytest' is not defined Traceback (most recent call last): File "/data/wang/gks/Development/effgen_dev/…edit textkit/__init__.py (+2/-1)--- a/textkit/__init__.py+++ b/textkit/__init__.py@@ -1,5 +1,6 @@ """Small text helpers."""  from .wrap import wrap_words+from .case import title_case -__all__ = ["wrap_words"]+__all__ = ["wrap_words", "title_case"]  ✓ Write textkit/__init__.py (+2/-1)╭────────────────────────────────── Recovered answer ──────────────────────────────────╮│ """Small text helpers."""                                                            ││                                                                                      ││ from .wrap import wrap_words                                                         ││                                                                                      ││ all = ["wrap_words"] | """Wrap a string onto lines of at most width characters."""   ││                                                                                      ││ def wrap_words(text: str, width: int = 40) -> list[str]: if not isinstance(text,     ││ str): raise TypeError("text must be a string") lines: list[str] = [] line = "" for   ││ word in text.split(): if line and len(line) + 1 + len(word) > width:                 ││ lines.append(line) line = word else: line = f"{line} {word}".strip() if line:        ││ lines.append(line) return lines | /data/wang/gks/effgen-demo/textkit/textkit/case.py ││ | from textkit import wrap_words                                                     ││                                                                                      ││ def test_wraps_at_width(): assert wrap_words("a b c d e", width=3) == ["a b", "c d", ││ "e"] | /data/wang/gks/effgen-demo/textkit/textkit/init.py                            │╰──────────────────────────────────────────────────────────────────────────────────────╯✓ Done in 69.5s · 6 tools · 14,523 tokens · $0.0056Tool calling: hybrid — provider tool API first, falling back to the text⚠ The model did not write this answer; it is the last tool result, after the model repeated the same call.Files written in /data/wang/gks/effgen-demo/textkit: textkit/case.py, textkit/__init__.py

Exit codes

CodeTypeDescription
0Completed.
1Failed.
2Completed, but the changes were withheld because there was no terminal to confirm on and neither --auto-edit nor --yes was given. A --commit that could not be confirmed exits here too.

Before the first call, not after the last

Not every model can complete a coding turn: some receive no tool definitions at all because their chat template renders none, some answer the question directly, and some write the call out as prose. Each of those finishes with an answer, no files written and exit 0 — which reads like success. So the command says one line up front when the chosen model is a poor fit, carrying the date it was measured. It never blocks the run, -q suppresses it, and effgen models info shows the same verdict as a Coding row.

Options

Every flag effgen code takes

Read out of effgen code --help in effGen 1.0.0, in the order it prints them, with its wording rather than a paraphrase.

FlagTypeDescription
taskWhat to build, change or debug (omit on a terminal to open an interactive session)
-p [TASK], --print [TASK]Run one task and print the result. Takes the task directly, or reads it from stdin when given alone.
-m MODEL, --model MODELModel to use
--provider PROVIDERProvider for a bare model id (e.g. openai, groq, cerebras, gemini, together, fireworks, replicate, anthropic, hf). Equivalent to the "provider:model" prefix.
-w DIR, --workspace DIRDirectory the agent reads and writes (created if missing). Sets EFFGEN_WORKSPACE for the run; nothing outside it is written.
--planPropose the change without writing a file or running a command
--auto-editApply file writes and sandboxed runs without asking; shell commands still need confirmation
-y, --yesApply writes, sandboxed runs and shell commands without asking (still confined to the workspace)
--review [TARGET]Review instead of change: read-only, with no tool that writes a file or runs a command. TARGET is uncommitted (the default), staged, or any revision or range git accepts (HEAD~3, main...HEAD). Combine with -f/--file, or use -f/--file alone to review files outside a repository.
-f PATH, --file PATHInclude a workspace file in the review, in full. Repeatable. Used with --review, or on its own to review files without a diff.
--session-id ID, --resume IDContinue a persistent session by id (the same store as `effgen chat --session-id` and `effgen sessions list`). Prior turns are recalled and new ones saved, along with the workspace, files in context and files written; a new id starts a fresh session.
--commitAfter the run, offer to commit the files it wrote (y/N; needs --yes without a terminal). Only those files are committed, and it never pushes, amends, resets or discards your work.
--commit-message MSGCommit message for --commit (default: a message naming the changed files and the task)
--undoReverse the last applied edit(s) in the workspace instead of running a task, restoring the previous file content
--undo-count NWith --undo, how many recent edits to reverse (default 1)
--max-iterations MAX_ITERATIONSIteration cap for the plan/run/fix loop (default: the coding preset's)
--temperature TEMPERATURETemperature
--max-tokens MAX_TOKENSMax output tokens per call (raise for reasoning models that spend part of the budget before any visible text)
--jsonEmit the result as one JSON document on stdout (answer, files_written, diffs, actions, tokens, cost). Human output goes to stderr.
-v, --verboseVerbose output (show DEBUG/INFO logs)
-q, --quietQuiet output (answer only)
--no-animationDisable live spinners/progress animation

effgen code --help · effGen 1.0.0

One short flag to watch

-p means --print here and --port on serve, monitor and top, so effgen code -p 8000 and effgen serve -p 8000 mean unrelated things. In a script, prefer the long spelling. The binding is frozen and a test pins it, so a third meaning cannot be added quietly.

The reference for effgen code

The workspace resolution order, the sandbox states and what each enforces, the full session table, the review contract, and the JSON document field by field.

docs/cli/code.md