github/java-coding-skill
Use this skill whenever editing `*.java` files in the `java/` directore of the SDK in order to write idiomatic, well-structured Java code for the Copilot SDK
npx skills add https://github.com/github/copilot-sdk --skill java-coding-skill
maven.compiler.release 17 so that uses running JDK 17 can use the jar.CompletableFuture for all async operations.AutoCloseable for resource cleanup (try-with-resources).<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java</artifactId>
<version>${copilot-sdk-java.version}</version>
</dependency>
implementation "com.github:copilot-sdk-java:${copilotSdkJavaVersion}"
try (var client = new CopilotClient()) {
client.start().get();
// Use client...
}
When creating a CopilotClient, use CopilotClientOptions:
cliPath - Path to CLI executable (default: "copilot" from PATH)cliArgs - Extra arguments prepended before SDK-managed flagscliUrl - URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a processport - Server port (default: 0 for random, only when useStdio is false)useStdio - Use stdio transport instead of TCP (default: true)logLevel - Log level: "error", "warn", "info", "debug", "trace" (default: "info")autoStart - Auto-start server on first request (default: true)autoRestart - Auto-restart on crash (default: true)cwd - Working directory for the CLI processenvironment - Environment variables for the CLI processgitHubToken - GitHub token for authenticationuseLoggedInUser - Use logged-in gh CLI auth (default: true unless token provided)onListModels - Custom model list handler for BYOK scenariosvar options = new CopilotClientOptions()
.setCliPath("/path/to/copilot")
.setLogLevel("debug")
.setAutoStart(true)
.setAutoRestart(true)
.setGitHubToken(System.getenv("GITHUB_TOKEN"));
try (var client = new CopilotClient(options)) {
client.start().get();
// Use client...
}
For explicit control:
var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false));
client.start().get();
// Use client...
client.stop().get();
Use forceStop() when stop() takes too long.
Use SessionConfig for configuration. The permission handler is required:
var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setStreaming(true)
.setTools(List.of(...))
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.APPEND)
.setContent("Custom instructions"))
.setAvailableTools(List.of("tool1", "tool2"))
.setExcludedTools(List.of("tool3"))
.setProvider(new ProviderConfig().setType("openai"))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
sessionId - Custom session IDclientName - Application namemodel - Model name ("gpt-5", "claude-sonnet-4.5", etc.)reasoningEffort - "low", "medium", "high", "xhigh"tools - Custom tools exposed to the CLIsystemMessage - System message customizationavailableTools - Allowlist of tool namesexcludedTools - Blocklist of tool namesprovider - Custom API provider configuration (BYOK)streaming - Enable streaming response chunks (default: false)workingDirectory - Session working directorymcpServers - MCP server configurationscustomAgents - Custom agent configurationsagent - Pre-select agent by nameinfiniteSessions - Infinite sessions configurationskillDirectories - Skill SKILL.md directoriesdisabledSkills - Skills to disableconfigDir - Config directory pathhooks - Session lifecycle hooksonPermissionRequest - REQUIRED permission handleronUserInputRequest - User input handleronEvent - Event handler registered before session creationAll setters return SessionConfig for method chaining.
var session = client.resumeSession(sessionId, new ResumeSessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
session.getSessionId() - Get session identifiersession.send(prompt) / session.send(MessageOptions) - Send message, returns message IDsession.sendAndWait(prompt) / session.sendAndWait(MessageOptions) - Send and wait for response (60s timeout)session.sendAndWait(options, timeoutMs) - Send and wait with custom timeoutsession.abort() - Abort current processingsession.getMessages() - Get all events/messagessession.setModel(modelId) - Switch to a different modelsession.log(message) / session.log(message, "warning", false) / session.log(message, "error", false) - Log to session timeline with level "info", "warning", or "error"session.close() - Clean up resourcesUse CompletableFuture for waiting on session events:
var done = new CompletableFuture<Void>();
session.on(event -> {
if (event instanceof AssistantMessageEvent msg) {
System.out.println(msg.getData().content());
} else if (event instanceof SessionIdleEvent) {
done.complete(null);
}
});
session.send(new MessageOptions().setPrompt("Hello"));
done.get();
Use the typed on() overload for compile-time safety:
session.on(AssistantMessageEvent.class, msg -> {
System.out.println(msg.getData().content());
});
session.on(SessionIdleEvent.class, idle -> {
done.complete(null);
});
The on() method returns a Closeable:
var subscription = session.on(event -> { /* handler */ });
// Later...
subscription.close();
Use pattern matching (Java 17+) for event handling:
session.on(event -> {
if (event instanceof UserMessageEvent userMsg) {
// Handle user message
} else if (event instanceof AssistantMessageEvent assistantMsg) {
System.out.println(assistantMsg.getData().content());
} else if (event instanceof AssistantMessageDeltaEvent delta) {
System.out.print(delta.getData().deltaContent());
} else if (event instanceof ToolExecutionStartEvent toolStart) {
// Tool execution started
} else if (event instanceof ToolExecutionCompleteEvent toolComplete) {
// Tool execution completed
} else if (event instanceof SessionStartEvent start) {
// Session started
} else if (event instanceof SessionIdleEvent idle) {
// Session is idle (processing complete)
} else if (event instanceof SessionErrorEvent error) {
System.err.println("Error: " + error.getData().message());
}
});
Control how errors in event handlers are handled:
// Set a custom error handler
session.setEventErrorHandler(ex -> {
logger.error("Event handler error", ex);
});
// Or set the error propagation policy
session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
Set streaming(true) in SessionConfig:
var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setStreaming(true)
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
Handle both delta events (incremental) and final events:
var done = new CompletableFuture<Void>();
session.on(event -> {
switch (event) {
case AssistantMessageDeltaEvent delta ->
// Incremental text chunk
System.out.print(delta.getData().deltaContent());
case AssistantReasoningDeltaEvent reasoningDelta ->
// Incremental reasoning chunk (model-dependent)
System.out.print(reasoningDelta.getData().deltaContent());
case AssistantMessageEvent msg ->
// Final complete message
System.out.println("\n--- Final ---\n" + msg.getData().content());
case AssistantReasoningEvent reasoning ->
// Final reasoning content
System.out.println("--- Reasoning ---\n" + reasoning.getData().content());
case SessionIdleEvent idle ->
done.complete(null);
default -> { }
}
});
session.send(new MessageOptions().setPrompt("Tell me a story"));
done.get();
Note: Final events (AssistantMessageEvent, AssistantReasoningEvent) are ALWAYS sent regardless of streaming setting.
Use ToolDefinition.create() with JSON Schema parameters and a ToolHandler:
var tool = ToolDefinition.create(
"get_weather",
"Get weather for a location",
Map.of(
"type", "object",
"properties", Map.of(
"location", Map.of("type", "string", "description", "City name")
),
"required", List.of("location")
),
invocation -> {
String location = (String) invocation.getArguments().get("location");
return CompletableFuture.completedFuture("Sunny in " + location);
}
);
var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setTools(List.of(tool))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
Use getArgumentsAs() for deserialization into a typed record or class:
record WeatherArgs(String location, String unit) {}
var tool = ToolDefinition.create(
"get_weather",
"Get weather for a location",
Map.of(
"type", "object",
"properties", Map.of(
"location", Map.of("type", "string"),
"unit", Map.of("type", "string", "enum", List.of("celsius", "fahrenheit"))
),
"required", List.of("location")
),
invocation -> {
var args = invocation.getArgumentsAs(WeatherArgs.class);
return CompletableFuture.completedFuture(
Map.of("temp", 72, "unit", args.unit(), "location", args.location())
);
}
);
var override = ToolDefinition.createOverride(
"built_in_tool_name",
"Custom description",
Map.of("type", "object", "properties", Map.of(...)),
invocation -> CompletableFuture.completedFuture("custom result")
);
When Copilot invokes a tool, the client automatically:
A permission handler is mandatory when creating or resuming sessions:
// Approve all requests (for development/testing)
new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
// Custom permission logic
new SessionConfig()
.setOnPermissionRequest((request, invocation) -> {
if ("dangerous-action".equals(request.getKind())) {
return CompletableFuture.completedFuture(
new PermissionRequestResult().setKind(PermissionRequestResultKind.DENIED)
);
}
return CompletableFuture.completedFuture(
new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)
);
})
Handle user input requests from the agent:
new SessionConfig()
.setOnUserInputRequest((request, invocation) -> {
System.out.println("Agent asks: " + request.getQuestion());
String answer = scanner.nextLine();
return CompletableFuture.completedFuture(
new UserInputResponse()
.setAnswer(answer)
.setWasFreeform(true)
);
})
var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.APPEND)
.setContent("""
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
"""))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.REPLACE)
.setContent("You are a helpful assistant."))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
Attach files to messages using Attachment:
session.send(new MessageOptions()
.setPrompt("Analyze this file")
.setAttachments(List.of(
new Attachment("file", "/path/to/file.java", "My File")
))
);
Use the mode property in MessageOptions:
"enqueue" - Queue message for processing (default)"immediate" - Process message immediatelysession.send(new MessageOptions()
.setPrompt("...")
.setMode("enqueue")
);
Use sendAndWait() to send a message and block until the assistant responds:
// With default 60-second timeout
AssistantMessageEvent response = session.sendAndWait("What is 2+2?").get();
System.out.println(response.getData().content());
// With custom timeout
AssistantMessageEvent response = session.sendAndWait(
new MessageOptions().setPrompt("Write a long story"),
120_000 // 120 seconds
).get();
Sessions are independent and can run concurrently:
var session1 = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
var session2 = client.createSession(new SessionConfig()
.setModel("claude-sonnet-4.5")
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
session1.send(new MessageOptions().setPrompt("Hello from session 1"));
session2.send(new MessageOptions().setPrompt("Hello from session 2"));
Use custom API providers via ProviderConfig:
// OpenAI
var session = client.createSession(new SessionConfig()
.setProvider(new ProviderConfig()
.setType("openai")
.setBaseUrl("https://api.openai.com/v1")
.setApiKey("sk-..."))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
// Azure OpenAI
var session = client.createSession(new SessionConfig()
.setProvider(new ProviderConfig()
.setType("azure")
.setAzure(new AzureOptions()
.setEndpoint("https://my-resource.openai.azure.com")
.setDeployment("gpt-4"))
.setBearerToken("..."))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
var sessions = client.listSessions().get();
for (var metadata : sessions) {
System.out.println("Session: " + metadata.getSessionId());
}
client.deleteSession(sessionId).get();
var state = client.getState();
AutoCloseable subscription = client.onLifecycle(event -> {
System.out.println("Lifecycle event: " + event);
});
// Later...
subscription.close();
try {
var session = client.createSession(new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
session.sendAndWait("Hello").get();
} catch (ExecutionException ex) {
Throwable cause = ex.getCause();
System.err.println("Error: " + cause.getMessage());
} catch (Exception ex) {
System.err.println("Error: " + ex.getMessage());
}
Monitor SessionErrorEvent for runtime errors:
session.on(SessionErrorEvent.class, error -> {
System.err.println("Session Error: " + error.getData().message());
});
Use ping() to verify server connectivity:
var response = client.ping("test message").get();
// Get CLI version and protocol info
var status = client.getStatus().get();
// Check authentication status
var authStatus = client.getAuthStatus().get();
// List available models
var models = client.listModels().get();
ALWAYS use try-with-resources for automatic disposal:
try (var client = new CopilotClient()) {
client.start().get();
try (var session = client.createSession(new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) {
// Use session...
}
}
// Resources automatically cleaned up
If not using try-with-resources:
var client = new CopilotClient();
try {
client.start().get();
// Use client...
} finally {
client.stop().get();
}
CopilotClient and CopilotSessioncreateSession and resumeSessionCompletableFuture properly - call .get() to block, or chain with .thenApply()/.thenCompose()sendAndWait() for simple request-response patterns instead of manual event handlingSessionErrorEvent for robust error handlingCloseable) when no longer neededSystemMessageMode.APPEND to preserve safety guardrails10. Provide descriptive tool names and descriptions for better model understanding
11. Handle both delta and final events when streaming is enabled
12. Use getArgumentsAs() for type-safe tool argument deserialization
13. Run Spotless before committing: CI will fail if mvn spotless:check fails. Before committing Java changes, run cd java && mvn spotless:apply and include the resulting changes in the commit.
try (var client = new CopilotClient()) {
client.start().get();
try (var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) {
var response = session.sendAndWait("What is 2+2?").get();
System.out.println(response.getData().content());
}
}
try (var client = new CopilotClient()) {
client.start().get();
try (var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) {
var done = new CompletableFuture<Void>();
session.on(AssistantMessageEvent.class, msg ->
System.out.println(msg.getData().content()));
session.on(SessionIdleEvent.class, idle ->
done.complete(null));
session.send(new MessageOptions().setPrompt("What is 2+2?"));
done.get();
}
}
try (var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) {
var response1 = session.sendAndWait("What is the capital of France?").get();
System.out.println(response1.getData().content());
var response2 = session.sendAndWait("What is its population?").get();
System.out.println(response2.getData().content());
}
record UserInfo(String id, String name, String email, String role) {}
var tool = ToolDefinition.create(
"get_user",
"Retrieve user information",
Map.of(
"type", "object",
"properties", Map.of(
"userId", Map.of("type", "string", "description", "User ID")
),
"required", List.of("userId")
),
invocation -> {
String userId = (String) invocation.getArguments().get("userId");
return CompletableFuture.completedFuture(
new UserInfo(userId, "John Doe", "[email protected]", "Developer")
);
}
);
var session = client.createSession(new SessionConfig()
.setModel("gpt-5")
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setHooks(new SessionHooks()
.setOnPreToolUse((input, invocation) -> {
System.out.println("About to execute tool: " + input);
var decision = new PreToolUseHookOutput().setKind("allow");
return CompletableFuture.completedFuture(decision);
})
.setOnPostToolUse((output, invocation) -> {
System.out.println("Tool execution complete: " + output);
return CompletableFuture.completedFuture(null);
}))
).get();
Take github/java-coding-skill from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.