Clojure SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.
Note: This SDK is in technical preview and may change in breaking ways.
Java users: See README-java.md for the Java API documentation.
Add to your deps.edn:
net.clojars.krukow/copilot-sdk {:mvn/version "0.1.0-SNAPSHOT"};; Git dependency (use a SHA)
io.github.krukow/copilot-sdk {:git/url "https://github.com/krukow/copilot-sdk-clojure.git"
:git/sha "d5796a529cca7943dbf5ff02936cd3b41c8e5ec4"}Update the SHA automatically:
bb readme:sha(require '[krukow.copilot-sdk :as copilot])
;; Create and start client
(def client (copilot/client {:log-level :info}))
(copilot/start! client)
;; Create a session
(def session (copilot/create-session client {:model "gpt-5.2"}))
;; Wait for response using events
(require '[clojure.core.async :refer [chan tap go-loop <!]])
(let [events-ch (chan 100)
done (promise)]
(tap (copilot/events session) events-ch)
(go-loop []
(when-let [event (<! events-ch)]
(case (:type event)
:assistant.message (println (get-in event [:data :content]))
:session.idle (deliver done true)
nil)
(recur)))
;; Send a message and wait for completion
(copilot/send! session {:prompt "What is 2+2?"})
@done)
;; Clean up
(copilot/destroy! session)
(copilot/stop! client)Or use the simpler blocking API:
;; Send and wait for response in one call
(def response (copilot/send-and-wait! session {:prompt "What is 2+2?"}))
(println (get-in response [:data :content]))
;; => "4"(copilot/client options)Options:
| Key | Type | Default | Description |
|---|---|---|---|
:cli-path |
string | "copilot" |
Path to CLI executable |
:cli-args |
vector | [] |
Extra arguments prepended before SDK-managed flags |
:cli-url |
string | nil | URL of existing CLI server (e.g., "localhost:8080"). When provided, no CLI process is spawned |
:port |
number | 0 |
Server port (0 = random) |
:use-stdio? |
boolean | true |
Use stdio transport instead of TCP |
:log-level |
keyword | :info |
One of :none :error :warning :info :debug :all |
:auto-start? |
boolean | true |
Auto-start server on first operation |
:auto-restart? |
boolean | true |
Auto-restart on crash |
:cwd |
string | nil | Working directory for CLI process |
:env |
map | nil | Environment variables |
(copilot/start! client)Start the CLI server and establish connection. Blocks until connected.
(copilot/with-client [client {:log-level :info}]
;; use client
)Create a client, start it, and ensure stop! runs on exit.
(copilot/stop! client)Stop the server and close all sessions gracefully.
(copilot/force-stop! client)Force stop the CLI server without graceful cleanup. Use when stop! takes too long.
(copilot/create-session client config)Create a new conversation session.
(copilot/with-session [session client {:model "gpt-5.2"}]
;; use session
)Create a session and ensure destroy! runs on exit.
Config:
| Key | Type | Description |
|---|---|---|
:session-id |
string | Custom session ID (optional) |
:model |
string | Model to use ("gpt-5.2", "claude-sonnet-4.5", etc.) |
:tools |
vector | Custom tools exposed to the CLI |
:system-message |
map | System message customization (see below) |
:available-tools |
vector | List of allowed tool names |
:excluded-tools |
vector | List of excluded tool names |
:provider |
map | Provider config for BYOK |
:mcp-servers |
map | MCP server configs keyed by server ID |
:custom-agents |
vector | Custom agent configs |
:on-permission-request |
fn | Permission handler function |
:streaming? |
boolean | Enable streaming deltas |
:config-dir |
string | Override config directory for CLI |
:skill-directories |
vector | Additional skill directories to load |
:disabled-skills |
vector | Disable specific skills by name |
:large-output |
map | Tool output handling config |
(copilot/resume-session client session-id)
(copilot/resume-session client session-id config)Resume an existing session by ID.
(copilot/ping client)
(copilot/ping client message)Ping the server to check connectivity. Returns {:message "..." :timestamp ... :protocol-version ...}.
(copilot/get-status client)Get CLI status including version and protocol information. Returns {:version "0.0.389" :protocol-version 2}.
(copilot/get-auth-status client)Get current authentication status. Returns:
{:authenticated? true
:auth-type :user ; :user | :env | :gh-cli | :hmac | :api-key | :token
:host "github.com"
:login "username"
:status-message "Authenticated as username"}(copilot/list-models client)List available models with their metadata. Requires authentication. Returns a vector of model info maps:
[{:id "gpt-5.2"
:name "GPT-5.2"
:vendor "openai"
:family "gpt-5.2"
:version "gpt-5.2"
:max-input-tokens 128000
:max-output-tokens 16384
:preview? false
:vision-limits {:supported-media-types ["image/png" "image/jpeg"]
:max-prompt-images 10
:max-prompt-image-size 20971520}}
...](copilot/state client)Get current connection state: :disconnected | :connecting | :connected | :error
(copilot/notifications client)Get a channel that receives non-session notifications. The channel is buffered; notifications are dropped if it fills.
(copilot/list-sessions client)List all available sessions. Returns vector of session metadata with
:start-time and :modified-time as java.time.Instant.
(copilot/delete-session! client session-id)Delete a session and its data from disk.
Represents a single conversation session.
(copilot/send! session options)Send a message to the session. Returns immediately with the message ID.
Options:
| Key | Type | Description |
|---|---|---|
:prompt |
string | The message/prompt to send |
:attachments |
vector | File attachments [{:type :file/:directory :path :display-name}] |
:mode |
keyword | :enqueue or :immediate |
(copilot/send-and-wait! session options)
(copilot/send-and-wait! session options timeout-ms)Send a message and block until the session becomes idle. Returns the final assistant message event.
(copilot/send-async session options)Send a message and return a core.async channel that receives all events for this message, closing when idle.
(copilot/send-async-with-id session options)Send a message and return {:message-id :events-ch} for correlating responses.
(copilot/events session)Get the core.async mult for session events. Use tap to subscribe:
(let [ch (chan 100)]
(tap (copilot/events session) ch)
(go-loop []
(when-let [event (<! ch)]
(println event)
(recur))))(copilot/events->chan session {:buffer 256
:xf (filter #(= :assistant.message (:type %)))})Subscribe to session events with optional buffer size and transducer.
(copilot/abort! session)Abort the currently processing message.
(copilot/get-messages session)Get all events/messages from this session.
(copilot/destroy! session)Destroy the session and free resources.
(copilot/session-id session)Get the session's unique identifier.
(copilot/client session)Get the client that owns this session.
Sessions emit various events during processing:
| Event Type | Description |
|---|---|
:user.message |
User message added |
:assistant.message |
Complete assistant response |
:assistant.message_delta |
Streaming response chunk |
:assistant.reasoning |
Model reasoning (if supported) |
:assistant.reasoning_delta |
Streaming reasoning chunk |
:tool.execution_start |
Tool execution started |
:tool.execution_progress |
Tool execution progress update |
:tool.execution_partial_result |
Tool execution partial result |
:tool.execution_complete |
Tool execution completed |
:session.idle |
Session finished processing |
Event :type values are keywords derived from the wire strings, e.g.
"assistant.message_delta" becomes :assistant.message_delta.
Enable streaming to receive assistant response chunks as they're generated:
(def session (copilot/create-session client
{:model "gpt-5.2"
:streaming? true}))
(let [ch (chan 100)]
(tap (copilot/events session) ch)
(go-loop []
(when-let [event (<! ch)]
(case (:type event)
:assistant.message_delta
;; Streaming chunk - print incrementally
(print (get-in event [:data :delta-content]))
:assistant.reasoning_delta
;; Streaming reasoning (model-dependent). Send to stderr.
(binding [*out* *err*]
(print (get-in event [:data :delta-content])))
:assistant.reasoning
(binding [*out* *err*]
(println "\n--- Final Reasoning ---")
(println (get-in event [:data :content])))
:assistant.message
;; Final complete message
(println "\n--- Final ---")
(println (get-in event [:data :content]))
nil)
(recur))))
(copilot/send! session {:prompt "Solve a logic puzzle and show your reasoning."})When :streaming? true:
:assistant.message_deltaevents contain incremental text in:delta-content:assistant.reasoning_deltaevents contain incremental reasoning in:delta-content(model-dependent)- Accumulate delta values to build the full response progressively
- The final
:assistant.messageevent always contains the complete content
(def client (copilot/client {:auto-start? false}))
;; Start manually
(copilot/start! client)
;; Use client...
;; Stop manually
(copilot/stop! client)Let the CLI call back into your process when the model needs capabilities you provide:
(def lookup-tool
(copilot/define-tool "lookup_issue"
{:description "Fetch issue details from our tracker"
:parameters {:type "object"
:properties {:id {:type "string"
:description "Issue identifier"}}
:required ["id"]}
:handler (fn [{:keys [id]} invocation]
(let [issue (fetch-issue id)]
(copilot/result-success issue)))}))
(def session (copilot/create-session client
{:model "gpt-5.2"
:tools [lookup-tool]}))When Copilot invokes lookup_issue, the SDK automatically runs your handler and responds to the CLI.
Handler return values:
| Return Type | Description |
|---|---|
| String | Automatically wrapped as success result |
Map with :result-type |
Full control over result metadata |
| core.async channel | Async result (yields string or map) |
Result helpers:
(copilot/result-success "It worked!")
(copilot/result-failure "It failed" "error details")
(copilot/result-denied "Permission denied")
(copilot/result-rejected "Invalid parameters")Control the system prompt:
(def session (copilot/create-session client
{:model "gpt-5.2"
:system-message
{:content "
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
"}}))The SDK auto-injects environment context, tool instructions, and security guardrails. Your :content is appended after SDK-managed sections.
For full control (removes all guardrails), use :mode :replace:
(copilot/create-session client
{:model "gpt-5.2"
:system-message {:mode :replace
:content "You are a helpful assistant."}})config-dir overrides where the CLI reads its config and state (e.g., ~/.copilot).
It does not define custom agents. Custom agents are provided via :custom-agents.
(def session (copilot/create-session client
{:model "gpt-5.2"
:config-dir "/tmp/copilot-config"
:skill-directories ["/path/to/skills" "/opt/team-skills"]
:disabled-skills ["legacy-skill" "experimental-skill"]}))Configure how large tool outputs are handled before being sent back to the model:
(def session (copilot/create-session client
{:model "gpt-5.2"
:large-output {:enabled true
:max-size-bytes 65536
:output-dir "/tmp/copilot-tool-output"}}))When a tool output exceeds the configured size, the CLI writes the full output to a temp file,
and the tool result delivered to the model contains a short message with the file path and preview.
You can see this message in :tool.execution_complete events:
(let [events (copilot/subscribe-events session)]
(go-loop []
(when-let [event (<! events)]
(when (= :tool.execution_complete (:type event))
(when-let [content (get-in event [:data :result :content])]
(println "Tool output message:\n" content)))
(recur))))Note: large output handling is applied by the CLI for built-in tools (like the shell tool). For external tools you define in the SDK, consider handling oversized outputs yourself (e.g., write to a file and return a short preview).
When the CLI needs approval (e.g., shell or file write), it sends a JSON-RPC
permission.request to the SDK. Your :on-permission-request callback must
return a map compatible with the permission result payload; the SDK wraps this
into the JSON-RPC response as {:result <your-map>}:
The permission_bash.clj example demonstrates both an allowed and a denied
shell command and prints the full permission request payload so you can inspect
fields like :full-command-text, :commands, and :possible-paths.
;; Approve
{:kind :approved}
;; Deny with rules
{:kind :denied-by-rules
:rules [{:kind "shell" :argument "echo hi"}]}
;; Deny without interactive approval
{:kind :denied-no-approval-rule-and-could-not-request-from-user}
;; Deny after user interaction (optional feedback)
{:kind :denied-interactively-by-user :feedback "Not allowed"}(def session1 (copilot/create-session client {:model "gpt-5.2"}))
(def session2 (copilot/create-session client {:model "claude-sonnet-4.5"}))
;; Both sessions are independent
(copilot/send-and-wait! session1 {:prompt "Hello from session 1"})
(copilot/send-and-wait! session2 {:prompt "Hello from session 2"})(copilot/send! session
{:prompt "Analyze this file"
:attachments [{:type :file
:path "/path/to/file.clj"
:display-name "My File"}]});; Connect to an existing CLI server (no process spawned)
(def client (copilot/client {:cli-url "localhost:8080"}))
(copilot/start! client)(try
(let [session (copilot/create-session client)]
(copilot/send! session {:prompt "Hello"}))
(catch Exception e
(println "Error:" (ex-message e))))See the examples/ directory for complete working examples:
| Example | Difficulty | Description |
|---|---|---|
basic_chat.clj |
Beginner | Simple Q&A conversation with multi-turn context |
tool_integration.clj |
Intermediate | Custom tools that the LLM can invoke |
multi_agent.clj |
Advanced | Multi-agent orchestration with core.async |
streaming_chat.clj |
Intermediate | Streaming deltas with incremental output |
config_skill_output.clj |
Intermediate | Config dir, skills, and large output settings |
permission_bash.clj |
Intermediate | Permission handling with bash |
Run examples:
clojure -A:examples -M -m basic-chat
clojure -A:examples -M -m tool-integration
clojure -A:examples -M -m multi-agent
clojure -A:examples -M -m streaming-chat
clojure -A:examples -M -m config-skill-output
clojure -A:examples -M -m permission-bashSee examples/README.md for detailed walkthroughs and explanations.
The SDK communicates with the Copilot CLI server via JSON-RPC:
Your Application
↓
Clojure SDK
↓ JSON-RPC (stdio or TCP)
Copilot CLI (server mode)
↓
GitHub Copilot API
The SDK manages the CLI process lifecycle automatically. You can also connect to an external CLI server via the :cli-url option.
This Clojure SDK provides equivalent functionality to the official JavaScript SDK, with idiomatic Clojure patterns:
| Feature | JavaScript | Clojure |
|---|---|---|
| Async model | Promises/async-await | core.async channels |
| Event handling | Callback functions | core.async mult/tap |
| Tool schemas | Zod or JSON Schema | JSON Schema (maps) |
| Blocking calls | await sendAndWait() |
send-and-wait! |
| Non-blocking | send() + events |
send! + events mult |
JavaScript:
import { CopilotClient, defineTool } from "@github/copilot-sdk";
import { z } from "zod";
const client = new CopilotClient();
await client.start();
const session = await client.createSession({
model: "gpt-5.2",
tools: [
defineTool("greet", {
description: "Greet someone",
parameters: z.object({ name: z.string() }),
handler: async ({ name }) => `Hello, ${name}!`
})
]
});
session.on((event) => {
if (event.type === "assistant.message") {
console.log(event.data.content);
}
});
await session.sendAndWait({ prompt: "Greet Alice" });
await session.destroy();
await client.stop();Clojure:
(require '[krukow.copilot-sdk :as copilot])
(require '[clojure.core.async :refer [chan tap go-loop <!]])
(def client (copilot/client {}))
(copilot/start! client)
(def greet-tool
(copilot/define-tool "greet"
{:description "Greet someone"
:parameters {:type "object"
:properties {:name {:type "string"}}
:required ["name"]}
:handler (fn [{:keys [name]} _]
(str "Hello, " name "!"))}))
(def session (copilot/create-session client
{:model "gpt-5.2"
:tools [greet-tool]}))
(let [ch (chan 100)]
(tap (copilot/events session) ch)
(go-loop []
(when-let [event (<! ch)]
(when (= (:type event) :assistant.message)
(println (get-in event [:data :content])))
(recur))))
(copilot/send-and-wait! session {:prompt "Greet Alice"})
(copilot/destroy! session)
(copilot/stop! client)# Run tests (unit + integration + example validation)
bb test
# Run tests with E2E (requires Copilot CLI)
COPILOT_E2E_TESTS=true COPILOT_CLI_PATH=/path/to/copilot bb test
# Generate API docs
bb docs
# Build JAR
bb ci
# Install locally
bb installAPI documentation is generated to doc/api/.
Set credentials:
export CLOJARS_USERNAME=your-username
export CLOJARS_PASSWORD=your-tokenThen publish:
bb jar
bb deploy:clojarsThe test suite includes unit, integration, example, and E2E tests (E2E disabled by default).
To enable E2E tests:
export COPILOT_E2E_TESTS=true
export COPILOT_CLI_PATH=/path/to/copilot # Optional, defaults to "copilot"
bb test- Clojure 1.12+
- JVM 11+
- GitHub Copilot CLI installed and in PATH (or provide custom
:cli-path)
- copilot-sdk - Official SDKs (Node.js, Python, Go, .NET)
- Copilot CLI - The CLI server this SDK controls
Copyright © 2026 Krukow
Distributed under the MIT License.