Build with CLDK: COCOA Code Context Agent
COCOA (COde COntext Agent) is a Claude Code plugin that runs CLDK before answering code-structure questions. It uses Bash heredocs to ask who calls a method, what a method calls, whether a sink is reachable, and which call sites a change may affect.
This guide builds COCOA manually: a manifest, a code-context subagent, and two skills, all built on the same CLDK heredoc. The plugin’s technical id is the lowercase cocoa, so its skills are /cocoa:callers and /cocoa:reachable.
How cocoa works
Section titled “How cocoa works”cocoa relies on a single mechanism: Claude Code’s Bash tool can run Python, so cocoa executes a one-shot CLDK script and reads its stdout. It requires no SDK integration and no running server.
flowchart LR
U([You]) --> CC[Claude Code]
CC -->|delegates| A["@code-context (cocoa)"]
A -->|"Bash: python3 - <<'PYEOF'"| K[CLDK analysis]
K -->|stdout| A
A -->|analysis result| CC
The plugin provides two components on top of that engine:
- a subagent (
code-context) for open-ended code-understanding questions; - skills (
/cocoa:callers,/cocoa:reachable): named, reusable operations that can be invoked directly.
The engine: CLDK in a heredoc
Section titled “The engine: CLDK in a heredoc”Every cocoa component runs a variant of this CLDK heredoc:
python3 - <<'PYEOF'from cldk import CLDKfrom cldk.analysis import AnalysisLevelimport networkx as nx
analysis = CLDK.java( project_path=".", analysis_level=AnalysisLevel.call_graph, # needed for callers/reachability)
callers = analysis.get_callers("org.apache.commons.cli.Options", "addOption(org.apache.commons.cli.Option)")for c in callers["caller_details"]: print(c["caller_method"].klass)PYEOFCOCOA in action
Section titled “COCOA in action”Once COCOA is installed, work continues in Claude Code. Claude can delegate to the code-context agent based on the request, or a skill such as /cocoa:reachable can be invoked directly. Three representative cases follow.
Generate a unit test from a method’s callees
Section titled “Generate a unit test from a method’s callees”COCOA supplies the target’s source body (get_method) together with the methods it invokes (get_callees), so a generated test exercises the method’s actual dependencies and signatures.
you ▸ Write a unit test for DefaultParser.parse(Options, String[]).
⤷ @code-context gather context for DefaultParser.parse(Options, String[]) ⤷ COCOA ran CLDK: get_method (the body) + get_callees (its collaborators)
Claude ▸ parse() calls Options.getOption, CommandLine.addOption, and Util.stripLeadingHyphens, and no external services, so nothing requires mocking. Here is a JUnit test that builds a real Options, parses argv, and asserts on the resulting CommandLine: …The test is derived from the method’s resolved callees rather than from assumed APIs.
Assess change impact before editing
Section titled “Assess change impact before editing”Before a signature change, COCOA enumerates the method’s callers (get_callers) so the edit can be scoped to the call sites that actually use it.
you ▸ I want to change Options.addOption(Option). What will break?
⤷ /cocoa:callers org.apache.commons.cli.Options addOption(org.apache.commons.cli.Option) ⤷ COCOA ran CLDK get_callers
Claude ▸ 7 call sites across 4 classes invoke it: Options.addOption(String, boolean, String), DefaultParser.handleOption, GnuParser.flatten, … I'll update exactly those and leave the rest untouched. Shall I make the change?The reported call sites come directly from the call graph.
Trace control flow through the call graph
Section titled “Trace control flow through the call graph”To understand an unfamiliar codebase, traverse the call graph (get_callers / get_callees) instead of reading files in isolation.
you ▸ How does option parsing flow through this project?
⤷ @code-context trace parsing starting at DefaultParser.parse ⤷ COCOA walked callers and callees via CLDK
Claude ▸ DefaultParser.parse is invoked by CommandLineParser implementations; it calls Options.getOption, CommandLine.addOption, and Util.stripLeadingHyphens. Here is the resolved path through the parser: …The trace is reconstructed from call-graph edges.
The pattern across all three cases is the same: a prompt triggers a CLDK query, and Claude answers from the returned methods or paths. The rest of this page wires that up.
Build the plugin
Section titled “Build the plugin”-
Scaffold the layout. A plugin is just a directory. Only
plugin.jsongoes inside.claude-plugin/; everything else sits at the root.Directorycocoa/
Directory.claude-plugin/
- plugin.json
Directoryagents/
- code-context.md
Directoryskills/
Directorycallers/
- SKILL.md
Directoryreachable/
- SKILL.md
- README.md
-
Write the manifest:
.claude-plugin/plugin.json. Onlynameis required; the rest is metadata for the plugin browser..claude-plugin/plugin.json {"name": "cocoa","displayName": "cocoa (Code Context Agent)","version": "0.1.0","description": "CLDK-powered code context: callers, reachability, and change impact from static analysis.","author": { "name": "Your Name" },"keywords": ["cldk", "static-analysis", "call-graph", "reachability"],"license": "Apache-2.0"} -
Add the Code Context subagent:
agents/code-context.md. The frontmatter declares its name, when Claude should delegate to it, and which tools it may use (Bashis the essential one, as it runs the heredoc). The body documents the CLDK pattern the subagent should follow.agents/code-context.md ---name: code-contextdescription: Answers codebase questions about callers, reachability, and change impact using CLDK static analysis. Delegate here before editing unfamiliar code.tools: Bash, Read, Grep, Globmodel: sonnet---You are a code-context agent. Compute call relationships and reachability withCLDK, then cite the result.To answer a question, run CLDK in a Python heredoc and read its stdout:```bashpython3 - <<'PYEOF'from cldk import CLDKfrom cldk.analysis import AnalysisLevelimport networkx as nxanalysis = CLDK.java(project_path=".", analysis_level=AnalysisLevel.call_graph,)# ... query analysis (get_callers / get_callees / get_call_graph) and print() ...PYEOF```Guidelines:- Build the analysis at `call_graph` level for any caller/callee/reachability query.- For reachability, use `networkx.has_path` over `analysis.get_call_graph()`.- Print exactly what you need; the stdout is your only observation.- Report file/line and method signatures from the CLDK output, never invent them. -
Add skills for named operations. Skills are namespaced by the plugin, so these become
/cocoa:callersand/cocoa:reachable.$ARGUMENTSis whatever the user types after the command.skills/callers/SKILL.md ---description: List every method that calls a target Java method, via CLDK.allowed-tools: Bash, Read---Find all callers of the method in: "$ARGUMENTS"(format: `fully.qualified.Class methodSignature` with fully-qualified parameter types,e.g. `org.apache.commons.cli.Options addOption(org.apache.commons.cli.Option)`)Run this, substituting the class and method, and report each caller:```bashpython3 - <<'PYEOF'from cldk import CLDKfrom cldk.analysis import AnalysisLevelanalysis = CLDK.java(project_path=".", analysis_level=AnalysisLevel.call_graph,)callers = analysis.get_callers("<CLASS>", "<METHOD>")for c in callers["caller_details"]:m = c["caller_method"]print(f"{m.klass} :: {m.method.signature}")PYEOF```skills/reachable/SKILL.md ---description: Decide whether a sink method is reachable from a source method in the call graph.allowed-tools: Bash, Read---Determine reachability for: "$ARGUMENTS"(format: `sourceClass sourceMethod -> sinkClass sinkMethod`)```bashpython3 - <<'PYEOF'from cldk import CLDKfrom cldk.analysis import AnalysisLevelimport networkx as nxanalysis = CLDK.java(project_path=".", analysis_level=AnalysisLevel.call_graph,)cg = analysis.get_call_graph()def node_for(cls, method):return next((n for n, d in cg.nodes(data=True)if cls in str(d) and method in str(d)), None)src, sink = node_for("<SRC_CLASS>", "<SRC_METHOD>"), node_for("<SINK_CLASS>", "<SINK_METHOD>")print("REACHABLE" if src and sink and nx.has_path(cg, src, sink) else "NOT REACHABLE")PYEOF``` -
(Optional) Greet on session start with a hook:
hooks/hooks.json. Use it to remind users that the commands are available.hooks/hooks.json {"hooks": {"SessionStart": [{ "hooks": [ { "type": "command","command": "echo 'cocoa ready: try @code-context, /cocoa:callers, /cocoa:reachable'" } ] }]}} -
Run it locally. Point Claude Code at the directory, no install needed while developing:
Terminal window claude --plugin-dir ./cocoaThen delegate to the agent or call a skill:
Terminal window > @code-context what calls Options.addOption, and can CLI.main reach CommandLine.execute?> /cocoa:callers org.apache.commons.cli.Options addOption(org.apache.commons.cli.Option)Edit a
SKILL.mdand/reload-pluginspicks it up; agent and hook changes need a session restart. -
Publish it. Validate, then add a marketplace entry so others can install it with the
/plugincommand.Terminal window claude plugin validate ./cocoa# share via a marketplace.json in your repo, then:/plugin marketplace add you/cocoa-plugin/plugin install cocoa@cocoa-plugin