Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ type Config struct {
}
```

- Loads from XDG config dir (`~/.config/fence/fence.json` or `~/Library/Application Support/fence/fence.json`) or custom path
- Loads from the default config path (Linux: `$XDG_CONFIG_HOME/fence/fence.json`, typically `~/.config/fence/fence.json`; macOS: `~/.config/fence/fence.json`) or a custom path
- Falls back to restrictive defaults (block all network, default command deny list)
- Validates paths and normalizes them

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ brew tap use-tusk/tap
brew install use-tusk/tap/fence
```

**NIX (macOS, Linux, Windows(WSL))**
**Nix (macOS, Linux, Windows (WSL)):**

```sh
nix run nixpkgs#fence -- --help
```

This runs it directly from the repository, without installing `fence`. If you want to install it, follow the guidelines [from NixOS](https://nix.dev) or [nix-darwin](https://github.com/nix-darwin/nix-darwin).

<details>
Expand Down Expand Up @@ -94,7 +95,7 @@ fence --help

### Configuration

Fence reads from `~/.config/fence/fence.json` by default (or `~/Library/Application Support/fence/fence.json` on macOS). See [configuration reference](./docs/configuration.md).
Fence reads from (`~/.config/fence/fence.json`) by default. See [configuration reference](./docs/configuration.md) for more details.

```json
{
Expand Down
11 changes: 6 additions & 5 deletions cmd/fence/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ func main() {
with network and filesystem restrictions.

By default, all network access is blocked. Configure allowed domains in
~/.config/fence/fence.json (or ~/Library/Application Support/fence/fence.json on macOS)
or pass a settings file with --settings, or use a built-in template with --template.
$XDG_CONFIG_HOME/fence/fence.json on Linux (typically
~/.config/fence/fence.json) or ~/.config/fence/fence.json on macOS, or pass
a settings file with --settings, or use a built-in template with --template.

Examples:
fence curl https://example.com # Will be blocked (no domains allowed)
Expand Down Expand Up @@ -196,7 +197,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to resolve extends: %w", err)
}
default:
configPath := config.DefaultConfigPath()
configPath := config.ResolveDefaultConfigPath()
cfg, err = config.Load(configPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
Expand Down Expand Up @@ -440,8 +441,8 @@ Examples:
fence import --claude

# Save to the default config path
# Linux: ~/.config/fence/fence.json
# macOS: ~/Library/Application Support/fence/fence.json
# Linux: $XDG_CONFIG_HOME/fence/fence.json (typically ~/.config/fence/fence.json)
# macOS: ~/.config/fence/fence.json
fence import --claude --save

# Save to a specific output file
Expand Down
9 changes: 8 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Configuration

Fence reads settings from `~/.config/fence/fence.json` by default (or `~/Library/Application Support/fence/fence.json` on macOS). Legacy `~/.fence.json` is also supported. Pass `--settings ./fence.json` to use a custom path. Config files support JSONC.
Fence reads settings from:

- Linux: `$XDG_CONFIG_HOME/fence/fence.json` (typically `~/.config/fence/fence.json`)
- macOS: `~/.config/fence/fence.json`
- Legacy paths still supported: macOS `~/Library/Application Support/fence/fence.json` and `~/.fence.json`
- Custom path: pass `--settings ./fence.json`

Config files support JSONC.

Example config:

Expand Down
23 changes: 20 additions & 3 deletions docs/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,19 @@ cfg.Network.AllowedDomains = []string{"example.com"}

Loads configuration from a JSON file. Supports JSONC (comments allowed).

This is a low-level loader and does not resolve `extends` entries relative to
the config file location. Use `LoadConfigResolved` if your config may use
relative `extends` paths.

#### `LoadConfigResolved(path string) (*Config, error)`

Loads configuration from a JSON file and resolves `extends` entries relative to
that file's parent directory. This matches the CLI's behavior.

```go
cfg, err := fence.LoadConfig(fence.DefaultConfigPath())
path := fence.ResolveDefaultConfigPath()

cfg, err := fence.LoadConfigResolved(path)
if err != nil {
log.Fatal(err)
}
Expand All @@ -94,7 +105,11 @@ if cfg == nil {

#### `DefaultConfigPath() string`

Returns the default config file path (`~/.config/fence/fence.json` on Linux, `~/Library/Application Support/fence/fence.json` on macOS, with fallback to legacy `~/.fence.json`).
Returns the canonical config file path for new configs (`$XDG_CONFIG_HOME/fence/fence.json` on Linux, typically `~/.config/fence/fence.json`; `~/.config/fence/fence.json` on macOS).

#### `ResolveDefaultConfigPath() string`

Returns the config path fence should load by default. It uses the canonical path (`$XDG_CONFIG_HOME/fence/fence.json` on Linux, typically `~/.config/fence/fence.json`; `~/.config/fence/fence.json` on macOS) when that file exists, and otherwise falls back to legacy macOS `~/Library/Application Support/fence/fence.json` and legacy `~/.fence.json` when those files exist.

#### `NewManager(cfg *Config, debug, monitor bool) *Manager`

Expand Down Expand Up @@ -271,7 +286,9 @@ wrapped, _ := manager.WrapCommand("npm run dev")
### Load and extend config

```go
cfg, err := fence.LoadConfig(fence.DefaultConfigPath())
path := fence.ResolveDefaultConfigPath()

cfg, err := fence.LoadConfigResolved(path)
if err != nil {
log.Fatal(err)
}
Expand Down
5 changes: 3 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ To update:
brew upgrade use-tusk/tap/fence
```

### NIX (macOS, Linux, Windows(WSL))
### Nix (macOS, Linux, Windows (WSL))

```sh
nix run nixpkgs#fence -- --help
```

This runs it directly from the repository, without installing `fence`. If you want to install it, follow the guidelines [from NixOS](https://nix.dev) or [nix-darwin](https://github.com/nix-darwin/nix-darwin).

### From Source
Expand Down Expand Up @@ -92,7 +93,7 @@ Create a starter config:
fence config init
```

By default, this writes `{"extends":"code"}` to `~/.config/fence/fence.json` (or `~/Library/Application Support/fence/fence.json` on macOS), so common coding workflows work out of the box.
By default, this writes `{"extends":"code"}` to `$XDG_CONFIG_HOME/fence/fence.json` on Linux (typically `~/.config/fence/fence.json`) and `~/.config/fence/fence.json` on macOS, so common coding workflows work out of the box.

If you want a starter file with empty arrays as editable hints, use:

Expand Down
88 changes: 64 additions & 24 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"slices"
"strings"

Expand Down Expand Up @@ -153,39 +154,78 @@ func Default() *Config {
}
}

// DefaultConfigPath returns the default config file path.
// Uses the OS-preferred config directory (XDG on Linux, ~/Library/Application Support on macOS).
// Falls back to ~/.fence.json if the new location doesn't exist but the legacy one does.
// DefaultConfigPath returns the canonical config file path for new configs.
// Uses ~/.config/fence/fence.json as the canonical path on macOS and the OS config dir elsewhere.
func DefaultConfigPath() string {
// Try OS-preferred config directory first
configDir, err := os.UserConfigDir()
if err == nil {
newPath := filepath.Join(configDir, "fence", "fence.json")
if _, err := os.Stat(newPath); err == nil {
return newPath
home, _ := os.UserHomeDir()
configDir, _ := os.UserConfigDir()

return defaultConfigPathFor(runtime.GOOS, home, configDir)
}

// ResolveDefaultConfigPath returns the config path fence should load by default.
// It prefers the canonical path when that file exists, but falls back to legacy
// locations while migrating older configs.
func ResolveDefaultConfigPath() string {
home, _ := os.UserHomeDir()
configDir, _ := os.UserConfigDir()

return resolveDefaultConfigPathFor(runtime.GOOS, home, configDir, pathExists)
}

func defaultConfigPathFor(goos, home, userConfigDir string) string {
canonicalPath := canonicalConfigPath(goos, home, userConfigDir)
if canonicalPath != "" {
return canonicalPath
}
return "fence.json"
}

func resolveDefaultConfigPathFor(goos, home, userConfigDir string, exists func(string) bool) string {
canonicalPath := defaultConfigPathFor(goos, home, userConfigDir)
if canonicalPath != "fence.json" {
if exists(canonicalPath) {
return canonicalPath
}
// Check if parent directory exists (user has set up the new location)
// If so, prefer this even if config doesn't exist yet
if _, err := os.Stat(filepath.Dir(newPath)); err == nil {
return newPath
}

for _, legacyPath := range legacyConfigPaths(goos, home) {
if exists(legacyPath) {
return legacyPath
}
}

// Fall back to legacy path if it exists
home, err := os.UserHomeDir()
if err != nil {
return "fence.json"
return canonicalPath
}

func canonicalConfigPath(goos, home, userConfigDir string) string {
switch {
case goos == "darwin" && home != "":
return filepath.Join(home, ".config", "fence", "fence.json")
case userConfigDir != "":
return filepath.Join(userConfigDir, "fence", "fence.json")
case home != "":
return filepath.Join(home, ".config", "fence", "fence.json")
default:
return ""
}
legacyPath := filepath.Join(home, ".fence.json")
if _, err := os.Stat(legacyPath); err == nil {
return legacyPath
}

func legacyConfigPaths(goos, home string) []string {
if home == "" {
return nil
}

// Neither exists, prefer new XDG-compliant path
if configDir != "" {
return filepath.Join(configDir, "fence", "fence.json")
paths := make([]string, 0, 2)
if goos == "darwin" {
paths = append(paths, filepath.Join(home, "Library", "Application Support", "fence", "fence.json"))
}
return filepath.Join(home, ".config", "fence", "fence.json")
return append(paths, filepath.Join(home, ".fence.json"))
}

func pathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

// Load loads configuration from a file path.
Expand Down
Loading
Loading