copilot-plus is a thin Bash wrapper around the GitHub Copilot CLI.
It exists to make Copilot runs safer and more consistent by auto-applying launch policy, preserving project scope, and exposing inspectable wrapper controls.
If you mainly want the modern Copilot/native split, jump to Configuration Ownership.
- Wraps
copilotand preserves native CLI behavior by default. - Injects wrapper-managed launch flags automatically from global and project policy.
- Adds wrapper-only operational helpers for visibility and debugging.
- Keeps wrapper options in a separate namespace so upstream flag collisions are avoided.
This project is named copilot-plus because it is the base tool plus wrapper behavior.
Design rule:
- Native Copilot options stay native.
- Wrapper-only options use
+/++prefixes (for example++cmd,++env,++verbose).
This is a general wrapper pattern: reserve a dedicated option namespace so wrapped-tool flags remain untouched and future-compatible.
We do not want to maintain this wrapper forever, but broad deprecation is not justified yet.
copilot-plus started as a workaround for early Copilot CLI versions that lacked durable settings, repo-local settings, custom instructions, trusted folders, and remembered permissions.
Modern Copilot CLI now provides a substantial native configuration surface:
~/.copilot/settings.json.github/copilot/settings.json.github/copilot/settings.local.json- repository instructions via
copilot init - trusted folders and remembered permissions
That closes many of the original gaps, but it does not replace this wrapper completely.
The wrapper is still useful for:
- startup injection of CLI-only launch policy such as
--allow-tool,--deny-tool,--available-tools,--excluded-tools,--add-dir, and--max-autopilot-continues - consistent global + project merge behavior for that policy
- dry-run and inspection helpers such as
++cmd,++env,++verbose, andCOPILOT_ARGS
The main modernization target is separating wrapper policy from upstream-managed Copilot config files more cleanly.
- YOLO-style defaults (minimal or no permission controls) are not acceptable for shared environments.
- Permissive-by-default automation is convenient short term but creates avoidable risk and cleanup cost.
copilot-plusmakes the safer path the default: explicit allow/deny controls, project scoping, and auditable effective args.
Security failures here are usually workflow failures first.
- Too many approval prompts create approval fatigue.
- Approval fatigue pushes people toward broad allow-lists or YOLO mode to reduce interruption.
- YOLO mode removes meaningful guardrails exactly when they are needed most.
Our policy is a deliberate middle ground:
- Keep enough permission friction to prevent dangerous defaults.
- Reduce repetitive approvals by codifying stable allow/deny policy in config.
- Keep runtime behavior auditable with
++cmd,++env, and++verbose.
This is neither maximum lock-down nor run wild. It is controlled autonomy with explicit boundaries.
A possible future enhancement is to run Copilot inside a Linux sandbox wrapper (for example, a tool like fence) to enforce filesystem isolation at the OS boundary.
Possible goals:
- Explicitly deny agent access to sensitive directories by default.
- Prevent access to credential material such as
~/.aws,~/.ssh, and~/.gnupgunless intentionally allowed. - Add a defense layer that does not depend only on in-tool permission semantics.
This is future work, not part of the current implementation.
Prefer native Copilot settings whenever there is a documented settings.json key. Use wrapper policy only for startup behavior that still exists primarily as CLI flags.
| Prefer native Copilot | Keep wrapper-managed |
|---|---|
model |
--allow-tool, --deny-tool |
allowedUrls, deniedUrls |
--available-tools, --excluded-tools |
trustedFolders |
--add-dir |
askUser, screenReader |
--max-autopilot-continues |
theme, stream, logLevel, autoUpdate |
++cmd, ++env, ++verbose |
native hooks and .github/copilot/*.json settings |
COPILOT_ARGS and global+project launch-policy merge |
Rule of thumb:
- If Copilot documents a native
settings.jsonkey for a behavior, prefer native config. - If the behavior is primarily a launch-time CLI flag or wrapper-only inspection feature, keep it in wrapper policy under the nested
copilotPluskey inside existing Copilot settings files.
~/.copilot/settings.json.github/copilot/settings.json.github/copilot/settings.local.json
- Preferred global wrapper policy host:
~/.copilot/settings.jsonundercopilotPlus - Preferred shared repo wrapper policy host:
.github/copilot/settings.jsonundercopilotPlus - Preferred local repo wrapper policy host:
.github/copilot/settings.local.jsonundercopilotPlus - Legacy fallback paths still honored:
~/.copilot/config.jsonand.copilot/config.jsontop-level--...keys - Wrapper input is effectively JSONC: comments and trailing commas are accepted because the script normalizes through
hjsonbeforejq.
Important:
- Modern Copilot treats
~/.copilot/config.jsonas managed application state. copilot-plusnow prefers existing Copilotsettings.jsonfiles and only falls back to legacy top-level--...entries inconfig.jsonfor compatibility.
- Global wrapper policy:
~/.copilot/settings.jsonundercopilotPlus - Shared repo wrapper policy:
.github/copilot/settings.jsonundercopilotPlus - Local repo wrapper policy:
.github/copilot/settings.local.jsonundercopilotPlus
Rationale:
- extends existing Copilot config files instead of inventing another file
- keeps wrapper-only launch policy namespaced away from native settings
- still avoids normal use of upstream-managed
config.json
bashjqhjsonenvsubst(usually fromgettext)copilotonPATH
chmod +x ./copilot-plusAdd an alias in shell config:
alias copilot='copilot-plus'Reload shell.
Show wrapper help:
copilot ++helpPreview final command without executing:
copilot ++cmd "test prompt"Show computed config args only:
copilot ++envNote:
++verboseprints diagnostics and then still executescopilot.
Put native Copilot settings in ~/.copilot/settings.json:
Put wrapper launch policy in the current implementation paths only for behavior that does not have a documented native settings key.
Preferred global wrapper path:
~/.copilot/settings.json
{
"theme": "github-dark-tritanopia",
"copilotPlus": {
"--allow-tool": ["shell(git:*)", "write"],
"--deny-tool": ["shell(git push)"],
"--max-autopilot-continues": 3
}
}Preferred shared repo wrapper path:
.github/copilot/settings.json
{
"copilotPlus": {
"--add-dir": ["./scripts"]
}
}Preferred local repo override path:
.github/copilot/settings.local.json
{
"copilotPlus": {
"--model": "gpt-5.4"
}
}Legacy fallback paths still honored during migration, using top-level --... keys:
~/.copilot/config.json.copilot/config.json
Avoid repeating native settings such as model, allowedUrls, or trustedFolders in wrapper policy.
Run normally (wrapper injects launch policy automatically):
copilot "summarize this module"Preview then execute with diagnostics:
copilot ++cmd "do x"
copilot ++verbose "do x"Current implementation resolves wrapper policy in this order:
- Preferred global wrapper config:
~/.copilot/settings.jsonkeycopilotPlus, else legacy~/.copilot/config.json - Preferred shared repo wrapper config: nearest parent
.github/copilot/settings.jsonkeycopilotPlus, else legacy.copilot/config.json - Preferred local repo override: nearest parent
.github/copilot/settings.local.jsonkeycopilotPlus - Auto project scope: injects
--add-dir <PROJECT_ROOT>when project config root is found
Merge rules:
- Scalar key in both configs: project value overrides global value for that key.
- Array key in both configs: values are additive (global first, project second).
- Type conflict for the same key (array vs scalar): hard error.
Value mapping:
array-> repeated--key valuetrue-> bare--keyfalse-> omittedstring/number-> single--key valueobject/null/unsupported -> hard error
Notes:
- Only keys that start with
--are treated as injectable flags. - For
--add-dir, non-existent directories are omitted. - All string values pass through
envsubstbefore injection. - Avoid adding native Copilot settings here when a documented
settings.jsonhome exists.
The current intended layout is:
- Global wrapper policy:
~/.copilot/settings.jsonkeycopilotPlus - Shared repo wrapper policy: nearest parent
.github/copilot/settings.jsonkeycopilotPlus - Local repo wrapper override: nearest parent
.github/copilot/settings.local.jsonkeycopilotPlus - Legacy
config.jsonpaths remain fallback-only compatibility paths - Keep the same merge rules, value mapping, and auto-project-root
--add-dirbehavior unless there is an explicit product decision to change them
This preserves the useful startup-policy layer while moving normal use off Copilot's managed config.json and onto existing Copilot settings files.
++cmdprints the exact command that would run and exits.++envprintsCOPILOT_ARGSand exits.++verboseor+vprints diagnostics, then executes normally.++helpor+hshows wrapper help and exits.++versionor+Vprints wrapper version and exits.
All non-wrapper arguments are passed through to copilot unchanged.
copilot-plus exports COPILOT_ARGS as the shell-quoted, config-derived flags for the current context.
This is intended for nested or subagent invocations of copilot-plus so child calls can reuse the same policy without recomputing or dropping arguments.
- Invalid JSON or JSONC in wrapper config: exits with error before launching
copilot. - Unsupported config value type: exits with error.
- Key type conflict between global and project wrapper config: exits with error.
- Missing or unreadable wrapper config files: silent passthrough.
Run tests with Bats:
bats tests/copilot.batsThe test suite validates wrapper config parsing, merge behavior, option handling, and execution semantics.
This wrapper can shrink further or be retired when Copilot CLI natively provides all of the following with documented support:
- persistent config equivalents for startup tool and path policy currently expressed as CLI flags
- per-project launch-policy merge behavior equivalent to this wrapper
- equivalent observability and debuggability for effective runtime args
When those conditions are met, prefer upstream behavior and retire or reduce copilot-plus.
{ "model": "gpt-5.4", "allowedUrls": ["https://docs.github.com"], "trustedFolders": ["/abs/path/to/workspaces"] }