Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f973284
FEAT-immutable-db: Overhaul of the policy generation and storage to a…
tom-nash Mar 24, 2026
f80ff68
FEAT-notif-flag: Addition of a notify flag to file and command rules …
tom-nash Mar 24, 2026
f1351c8
FEAT-Major: Addition of developmental cordon-web authentication tool.…
tom-nash Mar 24, 2026
e6f8d01
FEAT-sync: Implement cordon sync with bidirectional policy sync, data…
tom-nash Mar 24, 2026
30646fa
FIX: Correcting even sync json tags to be snake_case instead of the G…
tom-nash Mar 24, 2026
f6d6dbf
FEAT-codex: Addresses issue #3 of the new Codex hook support. Removed…
tom-nash Mar 27, 2026
be2b1de
FEAT-codex: Addition of cordon mcp installation with codex support. R…
tom-nash Mar 27, 2026
3d5889f
CHORE: Adding .codex/ to the .gitignore
tom-nash Mar 27, 2026
021256d
FIX: Trying to fix a prompting issue whereby Codex in particular woul…
tom-nash Mar 27, 2026
bf3b528
FEAT: Addition of session_id and transcript_path to the look log table.
tom-nash Mar 28, 2026
17a72ab
FEAT-sessions: Adding session transcript support for claude, codex, g…
tom-nash Mar 28, 2026
6553e8e
FEAT-logging: Addition of session id, better timestamps and command p…
tom-nash Mar 28, 2026
adbc24c
FEAT-sync-store: Updated the store layer to accept limits and updated…
tom-nash Mar 28, 2026
e155baa
Normalize stored file paths to repo-relative
tom-nash Mar 29, 2026
2c92caf
Parse bash command segments with shell AST
tom-nash Mar 29, 2026
ab616c3
Enforce command rules for VS Code run_in_terminal
tom-nash Mar 29, 2026
3d8d98c
FEAT-BREAKING-command-parsing: Implemented shell op enforcement and h…
tom-nash Mar 29, 2026
3fa6cd4
REFACTOR-policy-sync: Policy hash-chain fields are now fully removed …
tom-nash Mar 29, 2026
efe6fb2
FEAT: Added persisted client_id to auth credentials. This will be use…
tom-nash Mar 29, 2026
91d3003
FEAT-logging: For denied hook entries, cordon log now prints a reason…
tom-nash Mar 29, 2026
07eac26
FIX-session-syncing: limit session extraction to recently active sess…
tom-nash Mar 29, 2026
ae4c415
FEAT: Added rule violation message to cordon log output
tom-nash Mar 29, 2026
1130fa7
Potential fix for pull request finding 'Writable file handle closed w…
tom-nash Mar 31, 2026
7dc532a
Potential fix for pull request finding 'Writable file handle closed w…
tom-nash Mar 31, 2026
746fa19
FEAT-secret-detection: redact secrets in command_raw/ops and add a co…
tom-nash Mar 31, 2026
c7c08a1
Merge branch 'cordon-web-sync' of https://github.com/cordon-co/cordon…
tom-nash Mar 31, 2026
6339ee7
FEAT-privacy: add best-effort repo path redaction in logged tool inpu…
tom-nash Mar 31, 2026
3544159
FIX-workflows: Test workflow was running twice on PR branches
tom-nash Mar 31, 2026
c8bfd39
FIX: Updating the agent support table in README.md
tom-nash Apr 2, 2026
8dddf1b
CHORE: Adding a note about possible DB compatability issues when upda…
tom-nash Apr 2, 2026
5dff87a
Fix formatting in upgrade note for clarity
tom-nash Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Test

on:
push:
branches:
- main
pull_request:

jobs:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
build/
.cordon/
.vscode/
.codex/
.cursor/
.opencode/
.gemini/
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The CLI is the core of the product. The extension is a thin UI layer that calls
## Core Concepts

- **Perimeter**: the top-level policy boundary for a repository
- **File Rule**: a file, folder, or glob pattern protected by an access policy. Standard rules (any member) or guardian rules (guardian/admin only)
- **File Rule**: a file, folder, or glob pattern protected by an access policy. Standard rules (any member) or elevated rules (elevated/admin only)
- **Pass**: a temporary access grant allowing an agent to write to a protected file. Configured with a duration
- **Demarcation**: a registered declaration of what an agent is currently working on, visible to the team via CodeLens and the demarcations panel

Expand All @@ -28,15 +28,15 @@ The CLI is the core of the product. The extension is a thin UI layer that calls
- Operational data (audit logs, pass state, demarcation history) is stored in `~/.cordon/repos/<repo-hash>/data.db` and never committed to the repo
- User credentials and global preferences are stored in `~/.cordon/`
- Hook integration is additive: Cordon appends its entries to existing hook configs without modifying other hooks
- Codex enforcement uses a managed `model_instructions_file` at `.cordon/codex-policy.md`
- Codex enforcement uses a PreToolUse hook in `.codex/hooks.json` with a feature flag in `.codex/config.toml`

## Enforcement Matrix

| Agent | Mechanism | Enforcement Level |
|-------|-----------|-------------------|
| Claude Code | PreToolUse hook via `cordon hook` | Hard (pre-execution block) |
| VS Code agents (Copilot) | PreToolUse hook via `cordon hook` | Hard (pre-execution block) |
| Codex | model_instructions_file + notify hook | Soft (model-compliant) |
| Codex | PreToolUse hook via `cordon hook` | Hard (pre-execution block) |
| Any MCP agent | Cordon MCP server | Soft (best-effort) |

## Additional Documentation
Expand Down
52 changes: 1 addition & 51 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,51 +1 @@
# Cordon Project Instructions

## Project Overview

Cordon (cordon.sh) is a developer tool that provides team-wide access policies and visibility for AI coding agents. It enforces file-level write restrictions across Claude Code, Codex, and VS Code Copilot using each tool's native hook mechanisms, with team-level policy distribution and audit logging.

## Repository Structure

This repo contains two packages:
- `cli/` — Go binary that serves as the CLI, hook enforcement engine, and MCP server
- `vs-code-extension/` — VS Code extension (TypeScript) that provides the IDE interface

The CLI is the core of the product. The extension is a thin UI layer that calls CLI commands with `--json` output.

## Core Concepts

- **Perimeter**: the top-level policy boundary for a repository
- **File Rule**: a file, folder, or glob pattern protected by an access policy. Standard rules (any member) or guardian rules (guardian/admin only)
- **Pass**: a temporary access grant allowing an agent to write to a protected file. Configured with a duration
- **Demarcation**: a registered declaration of what an agent is currently working on, visible to the team via CodeLens and the demarcations panel

## Key Architecture Decisions

- The CLI binary handles all business logic. The extension never calls the API directly — it calls CLI subcommands
- `cordon hook` is invoked as a PreToolUse hook by Claude Code and VS Code agents. It reads JSON from stdin, checks policy, returns allow/deny
- `cordon --mcp` runs as a stdio MCP server providing file rule checks, pass requests, and demarcation registration
- Policy is stored in SQLite: `.cordon/policy.db` in the repo for unauthenticated users, `~/.cordon/repos/<repo-hash>/policy-cache.db` for authenticated users synced from the cloud
- Operational data (audit logs, pass state, demarcation history) is stored in `~/.cordon/repos/<repo-hash>/data.db` and never committed to the repo
- User credentials and global preferences are stored in `~/.cordon/`
- Hook integration is additive: Cordon appends its entries to existing hook configs without modifying other hooks
- Codex enforcement uses a managed `model_instructions_file` at `.cordon/codex-policy.md`
## Enforcement Matrix

| Agent | Mechanism | Enforcement Level |
|-------|-----------|-------------------|
| Claude Code | PreToolUse hook via `cordon hook` | Hard (pre-execution block) |
| VS Code agents (Copilot) | PreToolUse hook via `cordon hook` | Hard (pre-execution block) |
| Codex | model_instructions_file + notify hook | Soft (model-compliant) |
| Any MCP agent | Cordon MCP server | Soft (best-effort) |

## Additional Documentation

For codebase cheatsheets, task lists, and detailed documentation, refer to the `agentdocs` repo.

## Code Conventions

- Go code in `cli/`: standard Go project layout, `go fmt`, no external dependencies unless necessary
- TypeScript code in `extension/`: standard VS Code extension patterns
- All CLI commands must support `--json` for structured output
- All user-facing output should be clean and minimal
- Error messages should be actionable
@AGENTS.md
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@
| Agent | Support | Hook Based Enforcement | MCP Elicitation Support |
|-------|---------|------------------------|-------------------------|
| <img height="20" src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/claude-color.svg" /> Claude Code | First class | ✓ Yes | ✓ Yes |
| <img height="20" src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/codex-color.svg" /> Codex | First class | ✓ Yes | ✓ Yes |
| <img height="20" src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/cursor.svg" /> Cursor | First class | ✓ Yes | ✓ Yes |
| <img height="20" src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/copilot-color.svg" /> VS Code Chat (Copilot) | First class | ✓ Yes | ✓ Yes |
| <img height="20" src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/gemini-color.svg" /> Gemini CLI | Effective | ✓ Yes | ⤫ No |
| <img height="20" src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/opencode.svg" /> OpenCode | Effective | ✓ Yes | ⤫ No |
| <img height="20" src="https://unpkg.com/@lobehub/icons-static-svg@latest/icons/codex-color.svg" /> Codex | Limited | ⤫ No | ⤫ No |

---

> [!NOTE]
> Upgrading from any version before `v0.6.x` may require deleting `~/.cordon/repos/` and/or `cordon uninstall && cordon init` in a repository to reset legacy databases.
> Database migrations and installation improvements are to be included from `v0.6.x` onward.

## Installation

**Quick install:**
Expand Down
3 changes: 1 addition & 2 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ internal/
store/ SQLite layer — policy.db (repo) and data.db (user)
hook/ hook evaluation logic
reporoot/ walks up to find .cordon/
claudecfg/ .claude/settings.local.json management
codexpolicy/ .cordon/codex-policy.md generation
config/ per-agent config file management (one file per platform)
flags/ shared flag state (avoids circular imports)
tests/
CLI integration tests — build binary, exercise via subprocess
Expand Down
15 changes: 15 additions & 0 deletions cli/cmd/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Package auth implements the "cordon auth" subcommand group.
package auth

import "github.com/spf13/cobra"

// Cmd is the parent "auth" command. Registered in cmd/root.go.
var Cmd = &cobra.Command{
Use: "auth",
Short: "Manage authentication",
Long: "Log in, log out, and check authentication status with Cordon Cloud.",
}

func init() {
Cmd.AddCommand(loginCmd, logoutCmd, statusCmd)
}
161 changes: 161 additions & 0 deletions cli/cmd/auth/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package auth

import (
"encoding/json"
"errors"
"fmt"
"os/exec"
"runtime"
"time"

"github.com/cordon-co/cordon-cli/cli/internal/api"
"github.com/cordon-co/cordon-cli/cli/internal/flags"
"github.com/spf13/cobra"
)

var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate via GitHub OAuth",
Long: "Starts a device OAuth flow — opens a browser to complete GitHub authorization and stores credentials in ~/.cordon/credentials.json.",
Args: cobra.NoArgs,
RunE: RunLogin,
}

// deviceResponse is the response from POST /api/v1/auth/device.
type deviceResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}

// tokenResponse is the success response from POST /api/v1/auth/token.
type tokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
User api.User `json:"user"`
}

// tokenErrorResponse is the error response from POST /api/v1/auth/token.
type tokenErrorResponse struct {
Error string `json:"error"`
}

type loginResult struct {
User api.User `json:"user"`
ExpiresAt time.Time `json:"expires_at"`
}

// RunLogin implements the login flow. Exported for use as a top-level alias.
func RunLogin(cmd *cobra.Command, args []string) error {
// Check if already logged in.
if api.IsLoggedIn() {
creds, _ := api.LoadCredentials()
if creds != nil {
if flags.JSON {
out, _ := json.MarshalIndent(loginResult{
User: creds.User,
ExpiresAt: creds.ExpiresAt,
}, "", " ")
fmt.Println(string(out))
return nil
}
fmt.Fprintf(cmd.ErrOrStderr(), "Already logged in as %s. Run \"cordon auth logout\" first to switch accounts.\n", creds.User.Username)
return nil
}
}

client := api.NewUnauthenticatedClient()

// Step 1: Start device flow.
var device deviceResponse
_, err := client.PostJSON("/api/v1/auth/device", map[string]string{"client_id": "cordon-cli"}, &device)
if err != nil {
return fmt.Errorf("auth login: start device flow: %w", err)
}

// Step 2: Display code and open browser.
if !flags.JSON {
fmt.Fprintf(cmd.OutOrStdout(), "\nOpen this URL in your browser: %s\n", device.VerificationURI)
fmt.Fprintf(cmd.OutOrStdout(), "Enter code: %s\n\n", device.UserCode)
fmt.Fprintln(cmd.OutOrStdout(), "Waiting for authorization...")
}
openBrowser(device.VerificationURI)

// Step 3: Poll for token.
interval := time.Duration(device.Interval) * time.Second
if interval < 1*time.Second {
interval = 5 * time.Second
}
deadline := time.Now().Add(time.Duration(device.ExpiresIn) * time.Second)

tokenReq := map[string]string{
"device_code": device.DeviceCode,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
}

for time.Now().Before(deadline) {
time.Sleep(interval)

var token tokenResponse
_, err := client.PostJSON("/api/v1/auth/token", tokenReq, &token)
if err != nil {
var apiErr *api.APIError
if errors.As(err, &apiErr) {
switch apiErr.Code {
case "authorization_pending":
continue
case "access_denied":
return fmt.Errorf("auth login: authorization denied by user")
case "expired_token":
return fmt.Errorf("auth login: device code expired, please try again")
}
}
// For non-API errors (network issues), keep polling.
continue
}

// Success — save credentials.
now := time.Now().UTC()
creds := &api.Credentials{
AccessToken: token.AccessToken,
User: token.User,
IssuedAt: now,
ExpiresAt: now.Add(time.Duration(token.ExpiresIn) * time.Second),
}
if err := api.SaveCredentials(creds); err != nil {
return fmt.Errorf("auth login: save credentials: %w", err)
}

if flags.JSON {
out, _ := json.MarshalIndent(loginResult{
User: creds.User,
ExpiresAt: creds.ExpiresAt,
}, "", " ")
fmt.Println(string(out))
return nil
}

fmt.Fprintf(cmd.OutOrStdout(), "Logged in as %s\n", creds.User.Username)
return nil
}

return fmt.Errorf("auth login: device code expired, please try again")
}

func openBrowser(url string) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", url)
case "linux":
cmd = exec.Command("xdg-open", url)
case "windows":
cmd = exec.Command("cmd", "/c", "start", url)
default:
return
}
_ = cmd.Start()
}
56 changes: 56 additions & 0 deletions cli/cmd/auth/logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package auth

import (
"encoding/json"
"fmt"

"github.com/cordon-co/cordon-cli/cli/internal/api"
"github.com/cordon-co/cordon-cli/cli/internal/flags"
"github.com/spf13/cobra"
)

var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Clear stored credentials",
Long: "Revokes the current token server-side and removes local credentials.",
Args: cobra.NoArgs,
RunE: RunLogout,
}

type logoutResult struct {
LoggedOut bool `json:"logged_out"`
}

// RunLogout implements the logout flow. Exported for use as a top-level alias.
func RunLogout(cmd *cobra.Command, args []string) error {
creds, err := api.LoadCredentials()
if err != nil {
return fmt.Errorf("auth logout: %w", err)
}
if creds == nil {
if flags.JSON {
out, _ := json.MarshalIndent(logoutResult{LoggedOut: false}, "", " ")
fmt.Println(string(out))
return nil
}
fmt.Fprintln(cmd.OutOrStdout(), "Not logged in.")
return nil
}

// Best-effort server-side revocation.
client := api.NewClientWithToken(creds.AccessToken)
_, _ = client.PostJSON("/api/v1/auth/revoke", nil, nil)

if err := api.ClearCredentials(); err != nil {
return fmt.Errorf("auth logout: %w", err)
}

if flags.JSON {
out, _ := json.MarshalIndent(logoutResult{LoggedOut: true}, "", " ")
fmt.Println(string(out))
return nil
}

fmt.Fprintln(cmd.OutOrStdout(), "Logged out.")
return nil
}
Loading
Loading