sipeed/picoclaw · error

failed to read security config: %w

Error message

failed to read security config: %w

What it means

Returned by loadSecurityConfig when os.ReadFile(securityPath) fails with an error other than not-exist (missing files are deliberately tolerated and return nil). This is an I/O-level failure reading security.yml: permission denied, the path is a directory, or another filesystem error. The underlying error is wrapped with %w so causes chain.

Source

Thrown at pkg/config/security.go:44

// securityPath returns the path to security.yml relative to the config file
func securityPath(configPath string) string {
	configDir := filepath.Dir(configPath)
	return filepath.Join(configDir, SecurityConfigFile)
}

// loadSecurityConfig loads the security configuration from security.yml
// and merges secure field values into the config.
func loadSecurityConfig(cfg *Config, securityPath string) error {
	if cfg == nil {
		return fmt.Errorf("config is nil")
	}

	data, err := os.ReadFile(securityPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("failed to read security config: %w", err)
	}

	// Save existing channels and ModelList before unmarshal
	savedChannels := make(ChannelsConfig, len(cfg.Channels))
	for name, bc := range cfg.Channels {
		savedChannels[name] = bc
	}
	// savedModelList := cfg.ModelList

	// Parse YAML into a yaml.Node tree to extract channels node
	var rootNode yaml.Node
	if err := yaml.Unmarshal(data, &rootNode); err != nil {
		return fmt.Errorf("failed to parse security config: %w", err)
	}

	// Extract channels node (support both 'channels' and 'channel_list' keys)
	var channelsNode *yaml.Node
	if len(rootNode.Content) > 0 {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check permissions: `ls -l <configdir>/security.yml` and `chmod 644` (or chown to the running user) so the process can read it
  2. If security.yml is a directory, remove/rename it and restore the real file
  3. Verify the path your app passes as securityPath actually points at the file you think it does (config dir env var, flag override)
  4. If the file is genuinely absent and you still get this, you are passing a path whose parent is unreadable — fix parent dir permissions

Example fix

# before (root-owned, mode 600)
-rw------- 1 root root security.yml

# after (readable by service user)
chown appuser:appuser security.yml && chmod 600 security.yml
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the file is readable as this process before loading config.
if info, err := os.Stat(securityPath); err == nil {
	if info.IsDir() {
		return fmt.Errorf("security config path %s is a directory", securityPath)
	}
	if f, err := os.Open(securityPath); err == nil {
		f.Close()
	} else {
		return fmt.Errorf("security config %s not readable: %w", securityPath, err)
	}
}

Try / catch

if err := loadSecurityConfig(cfg, securityPath); err != nil {
	if errors.Is(err, fs.ErrPermission) {
		// guide user to chmod/chown rather than raw error
		log.Fatalf("cannot read %s: fix ownership/permissions", securityPath)
	}
	return err
}

Prevention

When it happens

Trigger: security.yml exists but the process lacks read permission (mode 0600 owned by another user), security.yml is a directory, or the file sits on a mount with I/O errors. os.IsNotExist(err) is false in all these cases, so the error is returned instead of ignored.

Common situations: Running the service as a different user than the one that created security.yml (common after switching to a systemd unit or container user), restoring configs with wrong ownership from backups/tarballs, or a stale directory named security.yml.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/b59df23ed95bf90d. Report an issue: GitHub.