anomalyco / anomalyco/opencode

OpenCode 1.18 field notes: loops, origins, agents, sessions, plan mode, and privacy routing

Open
#50,073 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

OpenCode 1.18 field notes: loops, origins, agents, sessions, plan mode, and privacy routing

These are things that bit real users on OpenCode 1.18.x (verified through 1.18.31, Windows + local and cloud models). Written so you can use them without knowing our setup. The Plan-mode section is a full stock permission matrix, not a one-liner — several built-ins look locked down and are not.

This is one post, not a pile of new issues. Two earlier drafts (the doom_loop gap and silent project_id=global) are finished here instead of being filed as separate writeups. Where an upstream issue already exists, comment there rather than opening a duplicate.

Not claiming: native doom_loop is fixed. #25254 was auto-closed for inactivity. The same processor.ts logic is still what 1.18.31 runs.


1. doom_loop is blind across turns (and can miss even inside one message)

What's broken

Docs say doom_loop fires when the same tool repeats 3 times with identical input, then asks (default) or denys.

The detector in packages/opencode/src/session/processor.ts does not do that in the way people expect:

const parts = /* parts of the CURRENT assistant message only */
const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD) // 3

if (
  recentParts.length !== 3 ||
  !recentParts.every(
    (part) =>
      part.type === "tool" &&
      part.tool === value.name &&
      part.state.status !== "pending" &&
      JSON.stringify(part.state.input) === JSON.stringify(input),
  )
) {
  return // no doom_loop
}

Two independent misses, both described in #25254:

  1. Current-message only. Repeats across assistant turns never accumulate. One failed edit per message, five messages in a row → never 3 matching parts in one message.
  2. Slice, then every(). If the last three parts include step-start, text, or reasoning, every() is false even when the message also contains three identical tool calls.

A third gap the docs do not mention:

  1. Exact JSON only. edit fail → edit fail (same payload) → read never hits 3 identical calls. Slightly different oldString on the same file never hits either. That is a common real retry loop.

steps has no default. If you never set agent.<name>.steps, the agent iterates until the model stops or you interrupt. Official agents docs: unset steps → no iteration limit.

#25254 is closed by the 60-day stale bot. Fix PRs (#25255, #32089) did not land in 1.18.31. Treat the issue as still live.

Related: #32187 (granularity), #23531 (configurable threshold).

Repro

A. Production-shaped miss (cleanest)

  1. OpenCode 1.18.x. Do not set steps. Leave permission.doom_loop at "ask".
  2. Point the agent at a real file. Have it edit with an oldString that is not in the file.
  3. Typical live sequence: failed edit → identical failed editread (or a third edit with a different oldString).
  4. No doom_loop permission prompt. The run continues.

B. Identical batch can still miss

opencode run --agent build --format json --auto "Call edit five times with the exact same filePath/oldString/newString. oldString must not exist in the file."

On 1.18.31, five identical failed edits in one assistant message produced no doom_loop permission event in the JSON stream. A non-tool part in slice(-3) is enough to skip the check. --auto auto-approves asks, so the stream absence is the evidence — not a claim about a hidden TUI dialog.

C. No default step cap

opencode debug config / resolved agent: if you never set steps, the field is absent. Unset → unbounded iteration.

What actually stops the loop today

Native doom_loop is not something you can patch from config. Two user-space backstops work.

1. Session-wide plugin (put a .js / .ts in ~/.config/opencode/plugins/ — auto-loaded). --pure and --auto silently undo this. See section 11 before you copy a headless recipe.

  • Count identical edit / write / patch calls across the session, not one message. Throw on the 3rd.
  • Separately count failed edits on the same file even when newString changes. Throw on the 3rd.
  • Ignore read so rereading a file is not a false positive.

tool.execute.before throw aborts the call. tool.execute.after throw is not a reliable session stop — arm a flag in after and throw on the next before.

MCP tools (including Playwright) do not get these hooks. A browser loop is not caught by this plugin.

Minimal shape (no logging of arguments):

const LIMIT = 3
const EDIT = { edit: true, write: true, apply_patch: true, patch: true }
const sessions = new Map()

export const FailedEditLoopGuard = async () => ({
  "tool.execute.before": async (input, output) => {
    const st = sessions.get(input.sessionID) || { sig: "", n: 0, armed: false }
    sessions.set(input.sessionID, st)
    if (st.armed) throw new Error("LOOP_GUARD: too many failed edits on the same file")
    if (!EDIT[input.tool]) return
    const sig = JSON.stringify(output.args || {})
    st.n = sig === st.sig ? st.n + 1 : 1
    st.sig = sig
    if (st.n >= LIMIT) throw new Error("LOOP_GUARD: identical edit repeated")
  },
  "tool.execute.after": async (input, output) => {
    if (!EDIT[input.tool]) return
    const text = String(output?.output || output?.error || "").toLowerCase()
    const failed = Boolean(output?.error) || text.includes("not found") || text.includes("old_string")
    const st = sessions.get(input.sessionID)
    if (!st) return
    if (failed) {
      st.fails = (st.fails || 0) + 1
      if (st.fails >= LIMIT) st.armed = true
    } else {
      st.fails = 0
    }
  },
})

export default FailedEditLoopGuard

Verified on 1.18.31: 3rd identical failed edit returned LOOP_GUARD and the file was unchanged. Native doom_loop had not stopped the first two.

2. Hard steps cap (this is the unbounded-run backstop, even if the plugin misses):

{
  "permission": { "doom_loop": "ask" },
  "agent": {
    "build": { "steps": 50 },
    "plan": { "steps": 50 }
  }
}

A plugin config hook can fill steps: 50 on any agent that omitted it. Explicit steps must be left alone.

Verified on 1.18.31 with steps: 3: step 1 tool, step 2 tool, step 3 text-only CRITICAL - MAXIMUM STEPS REACHED. Remaining tool work did not run.

doom_loop: ask is already the documented default. Setting it only makes the config explicit. It does not widen the matcher.

Suggested upstream fix (comment on #25254 or reopen)
  • Filter matching tool+input first, then count; do not slice(-3) a mixed part list.
  • Count since the last user turn, not only ctx.assistantMessage.id.
  • Default steps (or a documented global cap) so unset config cannot run forever.
  • Optional second rule: same tool + same path + N consecutive failures, intervening read of that path does not reset.

2. Playwright MCP origin-lock vs real websites

What's broken

@playwright/mcp defaults to allow all origins. An --allowed-origins list is a deny-by-default allowlist. If you set it to one client site plus localhost (a common “be careful” instinct), the agent cannot open any other public URL. That is a hard blocker for research, QA, or visual review of arbitrary sites.

--allowed-origins is not a documented place for https://*. Supported forms are a full origin (https://example.com:8080) or a wildcard port (http://localhost:*). You cannot express “any https business site” as an allowlist without listing every host.

What to do instead

Drop --allowed-origins. Use --blocked-origins (evaluated first) for the origins you actually must not hit, and leave public https allowed:

{
  "mcp": {
    "playwright": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "@playwright/mcp@latest",
        "--isolated",
        "--blocked-origins",
        "file://;http://169.254.169.254;https://169.254.169.254;http://metadata.google.internal;http://metadata.google.internal.;http://100.100.100.200;https://100.100.100.200"
      ]
    }
  }
}

Why those blocks (SSRF-relevant, not theater):

Origin Why
file:// Browser navigating to local files. MCP also blocks file:// unless --allow-unrestricted-file-access; keep it on the list anyway.
169.254.169.254 Cloud instance-metadata (AWS / Azure / GCP). Classic SSRF target.
metadata.google.internal GCP metadata DNS name (trailing-dot variant too).
100.100.100.200 Alibaba metadata.

Leave localhost / 127.0.0.1 off the blocklist if you use Playwright to review a local preview server.

Microsoft’s own docs: origin filtering is not a security boundary and does not follow redirects. This is use-case scoping, not a sandbox. Pair it with OS egress controls if you need a real boundary.

Verified with the same flag string: https://example.com and another public site allowed; file:// and http://169.254.169.254/... blocked.


3. Desktop: custom agents collapse to built-in general (#29616)

What's broken

Custom agents in ~/.config/opencode/agents/*.md (or opencode.json) often work with:

opencode --agent your-agent

On Desktop, when the parent calls the task tool, subagent_type still only offers built-ins (explore, general, …). The model then roleplays: subagent_type: "general" plus a prompt like “act as <your-agent>”. You do not get that agent’s model, permission set, or prompt.

This is not “the parent model is too small.” Same miss with a local parent and with a high-capacity cloud parent. The enum never included the custom name.

Still present as of Desktop 1.18.21 (and the 1.18.x line). See also #31025, #26516.

Workaround
  • Prefer a skill (~/.config/opencode/skills/<name>/SKILL.md) when you need Desktop to follow a named workflow inside the current session.
  • Or start the session from CLI: opencode --agent <name> / opencode run --agent <name>.
  • Do not trust a Desktop task call that says it spawned your custom agent unless the tool payload’s subagent_type is actually that name.

4. Desktop Home list: no delete (#40786)

The Home session list has no delete/archive control (SHOW_HOME_SESSION_ARCHIVE is hardcoded false; rows have no context menu). Related: #38820, #33129, #41265.

Workaround — CLI, not sqlite:

opencode session list
opencode session delete ses_...

You can delete from an open session’s header menu in some builds. That still requires opening the session first. The Home list itself does not clean up.


5. Built-in permission matrix (Plan is not locked down)

Marketing and the agents docs disagree with each other, and both disagree with 1.18.31 source (packages/opencode/src/agent/agent.ts). The table below is the stock merge before your opencode.json overlays. Last matching rule wins; user permission and agent.<name>.permission are applied on top.

Shared base (every built-in starts here)
Action Stock
* (anything not listed) allow
read allow, except *.env / *.env.*ask; *.env.exampleallow
external_directory ask, except OpenCode tmp / truncate / skill dirs → allow
doom_loop ask
question deny
plan_enter deny
plan_exit deny

There is no stock deny on bash / edit / task / webfetch / websearch / skill. Those are allow unless a named agent overrides them.

What each shipped agent actually changes

Legend: A = allow, D = deny, Q = ask, = inherits the base (so allow, unless the row is already Q/D). edit covers edit / write / patch.

Action build plan general explore compaction / title / summary
Mode primary primary subagent subagent hidden primary
read / glob / grep / list — (A) — (A) — (A) A (re-allowed after *: deny) D (*: deny)
edit (workspace files) — (A) D, except .opencode/plans/*.md and the managed plan dir — (A) D (caught by *: deny) D
bash — (A) — (A) — (A) A D
webfetch / websearch — (A) — (A) — (A) A D
task / subagents — (A) D for general only; other names inherit A — (A) D (*: deny) D
todowrite — (A) — (A) D D D
question A A D (base) D D
plan_enter A D (base) D D D
plan_exit D (base) A D D D
external_directory Q (base) Q, plus managed plan dir A Q Q (readonly-style whitelist) D
doom_loop Q Q Q Q Q (irrelevant; no tools)
steps unset (unlimited) unset unset unset unset

compaction / title / summary are hidden primaries with *: deny. They are not modes you Tab into. Compaction still rewrites session history; it does not need tools to do that.

The two “looks locked, isn’t” rows

Plan. The point of the mode is “don’t write the repo.” That part is real. What people then assume — no shell, no network, no subagents, no search — is not. Stock plan still allows bash, webfetch, websearch, and read/grep/glob. The only extra task deny is task.general. Older agents-doc wording that Plan sets bash to ask is not what 1.18.31 ships. If you need a read-only planner, say so:

{
  "agent": {
    "plan": {
      "permission": {
        "edit": "deny",
        "bash": "deny",
        "webfetch": "deny",
        "websearch": "deny",
        "task": "deny"
      }
    }
  }
}

A planner that “won’t even open files” is the opposite mistake: read was never denied. Check for a custom overlay that denied it.

Explore. Docs call it read-only / “cannot modify files.” File writes are denied. bash is explicitly allowed. That is a full execute path on a subagent people spawn for “just look around.” Deny bash on explore if that is not what you meant.

General is not a sandbox either. It has the same *: allow as build except todowrite: deny. When Desktop’s task tool collapses a custom agent into general (section 3), you get this matrix — edits and shell included.

Hidden agents aside, nothing in the stock set is an air gap. *: allow is the default. Deny is opt-in.


6. Same model name, different provider: Zen/Go bypasses OpenRouter privacy

OpenCode can list GLM 5.3 Flash (and many others) under more than one provider group:

You pick Request goes to OpenRouter ZDR / data_collection
opencode/... (Zen) OpenCode Zen No
opencode-go/... (Go) OpenCode Go (https://opencode.ai/zen/go/v1/...) No
openrouter/... OpenRouter Yes, if you set them

Account-level OpenRouter privacy (ZDR, data_collection: deny, provider preferences) applies only to requests OpenRouter actually routes. Selecting the model from the Zen or Go catalog is a different HTTP path. The label in the picker can look like the same model.

To have ZDR apply:

  1. Select the model from the OpenRouter provider group (openrouter/<org>/<model>).
  2. Enforce ZDR on that path (provider.zdr: true and/or account privacy + data_collection: deny).

Zen/Go have their own terms and exceptions (some Go SKUs are explicitly not ZDR). Do not assume “I turned on OpenRouter ZDR, therefore every GLM call in OpenCode is ZDR.”

A provider.opencode.blacklist / provider.opencode-go.blacklist in opencode.json only hides those provider IDs. It does not move traffic onto OpenRouter.


7. Non-git folders silently share project_id=global (#18890)

Finished from the earlier draft; still the same mechanism.

If the working directory has no .git, Project.fromDirectory() assigns a hardcoded project_id = global (worktree often /). Sessions from unrelated non-git folders share one project bucket. There is no warning in CLI or Desktop.

That mixes session lists, --continue, and anything keyed on project_id (including which project opencode.json / default_agent you think you are using).

Related: #15719 (recent projects show /), #16812 / #21230 (git init does not reliably re-home old global sessions). TUI list filtering PRs reduce some leakage; they do not give each non-git directory its own id.

Repro
mkdir /tmp/oc-site-a /tmp/oc-site-b
echo a > /tmp/oc-site-a/index.html
echo b > /tmp/oc-site-b/index.html
opencode run --dir /tmp/oc-site-a "hello"
opencode run --dir /tmp/oc-site-b "hello"

Inspect the session table: both rows project_id = 'global'. No banner.

Optional: git init && git commit --allow-empty -m init in site-a, then another session in that folder or a subdir. New rows may still be global until a real project row exists and migrateFromGlobal() actually moves them.

Unrelated git repos on the same machine (id = root commit hash) are not in that bucket. Only unresolved / non-git ones.

Workaround (does not fix the silence)
  1. git init + first commit in the folder you want treated as a project.
  2. Add a project-level opencode.json if you need a pin (default_agent, MCP, …).
  3. Open / --dir that git root, not a random parent and not Desktop’s empty default folder.

This does not warn that you are already in global, does not always migrate old rows, and does not give each non-git directory its own id.

Suggested upstream
  • Derive a non-git project id from the resolved directory path (#18890).
  • Warn once when a session is created under the fallback.
  • Make migrateFromGlobal() move subdirectory sessions after git init (#21230).

8. Compaction can wipe an output contract mid-chain

compaction.auto defaults to true. There is no 75% knob in the 1.18.x schema; the trigger is a reserved-token buffer, not a percentage you set. compaction.prune defaults false.

On a long tool chain the hidden compaction agent rewrites history, then the session continues with a synthetic compaction_continue. The model still has “you were doing something,” but a strict output contract (fixed headings, four required lines, “end with exactly this schema”) is often gone. The work already done is real; the required closer is not.

Verified: a 30-read chain completed every named read, then compacted, then answered with analysis / “what next?” and never the required four-line footer. A sibling run that stayed under the buffer (larger context, no compaction_continue) kept the closer in play — and could still fail the format for model reasons. Treat those as different failures.

This compounds with section 1. A long retry loop fills the window, compaction fires, the “stop and report X” instruction disappears, and doom_loop still has not.

What to do:

  • Do not score a compacted run as “the model ignored the format.” Check the event stream for agent=compaction / compaction_continue first.
  • For contract-critical harnesses: "compaction": { "auto": false } on that run, or give the model enough context that the buffer never trips.
  • After a live compact, re-state the closer. Compaction is a history rewrite, not a reminder.

9. A new session from a child inherits the active agent, not default_agent

default_agent is used when a session is created with no agent already selected. It does not rewrite an agent stored on a session, and it is not re-read when you spawn a new session from inside an existing one.

If you are in a child (or you Tabbed the parent to build / a custom primary) and you start a new session from there, the new session keeps whatever agent was active. Project default_agent (for example plan, or a deny-all lean primary) is not consulted. Easy to file as a routing bug. It is session-create inheriting the current mode.

Reported as unexpected: #29594 (session_new from a switched parent). Related: #27370 (resumed sessions also keep their stored agent).

What to do: set the agent on the new session (--agent, Tab, Desktop picker) instead of assuming the project pin. default_agent still applies to a cold start from Home / opencode run with no --agent.

A subagent cannot be the session agent. --agent on a mode: subagent name falls back (usually to build / the first visible primary). That is a separate, also easy-to-miss, rule.


10. Other quietly consequential defaults

Small, verified, and harmless-looking until they combine with something above.

permission.mcp is not “all MCP tools.” It is a permission for a tool literally named mcp. Live MCP tools are servername_toolname (Playwright → playwright_browser_*). "permission": { "mcp": "deny" } on a lean agent does nothing to them; they inherit *: allow. Deny the actual prefix.

Desktop “hide model” ≠ opencode.json blacklist. Manage Models writes a Desktop visibility store. CLI opencode run --model … still sees every ID. provider.<id>.blacklist is the CLI/config gate. Two stores.

Windows: opencode.cmd plus < in the prompt silently truncates. cmd.exe treats <...> as redirection. A multiline Start-Process opencode … (PATH → .cmd) can deliver only the first sentence. The model then looks like it “ignored the file list.” Call opencode.exe with a prompt file; do not put < through the cmd shim.

--pure / --auto: not a one-liner. Section 11.

default_agent must be a visible primary. A subagent or hidden: true name falls back to build (with a warning). A deny-all custom primary as default_agent is valid — and then every consumer that does not pass --agent (including other apps that shell out to opencode) gets no tools.

explore allows bash. Covered in the matrix. Repeated here because “read-only subagent” is the phrase that causes the spawn.

Zen/Go blacklist ≠ OpenRouter hide. provider.opencode.blacklist does not remove openrouter/<same-name>. Privacy routing is still section 6.

opencode debug config expands env API keys into plaintext. Do not paste that dump. Prefer opencode debug agent <name> and quote only permission / steps / model-id fields.


11. --pure and --auto silently disable the loop-guard you just installed

These are official opencode run flags. Neither errors. Neither names what it turned off.

Flag What the help text says What it actually does to section 1
--pure “run without external plugins” Does not load ~/.config/opencode/plugins/ (or project .opencode/plugins/). The session-wide failed-edit guard from section 1 is not there. No “skipped N plugins” line.
--auto “auto-approve permissions that are not explicitly denied (dangerous!)” If native doom_loop ever asks, the ask is approved and the loop continues. The JSON stream may show no useful permission event.

Practical consequence: if you installed the plugin workaround in section 1, then ran a headless recipe that includes either flag, you are back to the original failure mode. No warning that the guard is gone. --pure removes the plugin. --auto auto-approves the native ask the plugin was meant to replace. Together they remove both user-space stops that still depend on plugins or ask.

What still works under those flags: an explicit agent.<name>.steps cap (section 1, backstop 2). That is config, not a plugin, and it is not a permission prompt. If steps is unset — the stock default — --pure and/or --auto leave you with nothing.

--pure is useful for “reproducible, no third-party code.” --auto is useful for CI. Both are legitimate. The miss is the silence when they override an active safety plugin or doom_loop: ask.

Do not copy opencode run --pure --auto … from a gist onto a machine whose only loop protection is that plugin.


12. Framework perspective (ideas, not a charge sheet)

The pattern in this post is the same one: a safety control exists, a second switch turns it off, and the user is not told. Maintainers may already have weighed these and rejected them for isolation, CI, or support reasons that are not obvious from outside. Offered as design questions, not as “you should have.”

  1. Warn when a flag overrides an active guard. If --pure skips loaded plugins, print one line: plugins not loaded (and a count). If --auto will auto-approve doom_loop, print that doom_loop asks will not stop the run. Same idea for Plan/Explore labels that do not match the permission matrix: a one-time doc or TUI hint beats discovering bash is allowed after a spawn.

  2. Let a plugin declare itself non-skippable. Something like required: true / safety: true on a plugin so --pure cannot drop a loop guard without an extra explicit --pure-unsafe (or a prompt). --pure can stay the default isolation tool; the distinction is “no convenience plugins” vs. “no safety hooks either.” Today that distinction does not exist.

  3. Default a harness-level cap, and keep ask out of --auto for loop detection. Unset steps plus current-message doom_loop plus --auto is three silent “continue”s stacked. A conservative default steps (overridable to unlimited) and a carve-out so --auto does not approve doom_loop would close the class without removing either flag. Compaction wiping a closer (section 8) is the same class: a system rewrite with no “your required format was dropped” receipt.

None of this replaces fixing #25254 in processor.ts. It is about making the overrides visible so a user-space workaround does not vanish.


Quick map

Symptom Likely cause What to do now Upstream
Agent retries the same failed edit forever doom_loop is current-message + exact JSON + slice-before-filter; steps unset Plugin + explicit steps Reopen / comment #25254
Plugin / doom_loop ask did nothing in a headless run --pure skipped plugins and/or --auto approved the ask Drop those flags, or keep steps set; see §11 Flags are silent overrides
Playwright can’t open any site except one allowlisted host --allowed-origins is deny-by-default Remove allowlist; --blocked-origins for file:// + cloud metadata Config, not an OpenCode bug
Desktop “spawned my custom agent” but used general task enum is built-ins Skills, or opencode --agent #29616
Can’t delete sessions from Desktop Home No list affordance opencode session delete <id> #40786
Plan / Explore “must be locked down” Stock *: allow; Plan denies writes only; Explore allows bash Explicit denials; do not trust the label Source agent.ts 1.18.31, not the older agents-doc ask list
Long run forgot the required footer Auto-compaction rewrote history (compaction_continue) Score only after checking compact events; compaction.auto: false for harnesses Config default, not a model bug
New session ignored default_agent session_new inherited the active agent Set --agent / picker on the new session #29594
Lean agent still called Playwright / other MCP permission.mcpplaywright_browser_* Deny the real tool prefix Docs vs live tool names
Windows prompt “lost” <...> opencode.cmd redirection opencode.exe + prompt file cmd.exe, not the model
OpenRouter ZDR didn’t apply to GLM You picked Zen/Go Pick openrouter/... Provider routing, not a toggle bug
Sessions from two non-git folders mixed Shared project_id=global git init + open the repo root #18890

Version / how this was checked

  • OpenCode 1.18.31 (CLI). Desktop custom-agent miss last confirmed on 1.18.21.
  • OS: Windows. Local OpenAI-compatible endpoint and cloud providers. Behavior is harness-side, not model-specific.
  • doom_loop source re-read from current processor.ts. Plugin + steps: 3 proven with live opencode run --format json (not config inspection alone). Playwright flags proven by driving @playwright/mcp navigate allow/block.
  • Permission matrix is the 1.18.31 agent.ts merge, not a custom overlay and not the older “Plan sets bash to ask” doc line. Compaction-vs-contract: live 30-read chain then compaction_continue, closer gone; larger-context sibling had no compact event.
  • No personal paths, account ids, or client names in this post. opencode debug config can expand env API keys into plaintext — do not paste that dump into GitHub.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

This post combines several independent reports, including packages/opencode/src/session/processor.ts for doom_loop behavior and packages/opencode/src/agent/agent.ts for permissions, plus Desktop and Playwright concerns. Start by selecting one behavior and its referenced upstream issue, then reproduce it with the commands or versions given. Done is not defined for the post as a whole because it contains multiple proposed fixes and workarounds.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, typescript
Domain
desktop, devtools, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.