Skip to content

policies-config.json always writes to global scope, ignoring --scope project/local #56

Description

@yashexosphere

Bug

writeHooksConfig() in src/hooks/hooks-config.ts hardcodes the global path ~/.failproofai/policies-config.json. When a user runs failproofai p -i --scope project, the Claude Code hooks are correctly written to {cwd}/.claude/settings.json, but the policies config always goes to the global file.

The read side (readMergedHooksConfig) properly merges 3 scopes (project → local → global), but the write side never creates project-level or local-level config files, so the merge logic is dead code in practice.

Reproduction

# Project A — user wants only block-sudo
cd ~/project-a
failproofai p -i block-sudo --scope project

# Project B — user wants only block-push-master
cd ~/project-b
failproofai p -i block-push-master --scope project

# Check: both policies are now in the GLOBAL config
cat ~/.failproofai/policies-config.json
# → {"enabledPolicies": ["block-sudo", "block-push-master"]}

# Both projects see BOTH policies — no isolation

Consequences

1. Policy leakage between projects

All policies accumulate in the single global config. Project A sees Project B's policies and vice versa. A security-sensitive project can't have a stricter policy set than a playground project — they all share the same list.

2. Uninstall in one project destroys another project's policies

cd ~/project-b
failproofai p -u block-sudo

This removes block-sudo from the global config — but Project A still needs it. Project A's security is silently degraded.

3. policyParams collision

If Project A wants block-push-master.protectedBranches: ["main", "release"] and Project B wants ["main", "staging"], only the last write survives. The other project silently gets the wrong config.

4. customPoliciesPath is global

failproofai p -i -c ./my-policies.js stores the absolute path in the global config. When the handler runs in a different project, it tries to load that file from the wrong project's path — either failing silently (fail-open = policies don't fire) or loading the wrong custom policies.

5. --scope all uninstall wipes all projects

removeHooks with scope === "all" clears enabledPolicies: [] in the global config. Every project loses its policies, not just the current one.

6. listHooks shows misleading per-scope columns

When hooks are installed at multiple scopes, listHooks shows per-scope columns (User/Project/Local) — but since there's only one config, every scope shows the same ON/OFF state. The UI implies per-scope policy control that doesn't exist.

7. Race condition on concurrent installs

Two terminals running failproofai p -i for different projects both read/modify/write the same global file. Last write wins, losing the first terminal's changes.

Root cause

// hooks-config.ts:76-78
function getConfigPath(): string {
  return resolve(homedir(), ".failproofai", "policies-config.json");
}

This function is used by both readHooksConfig() and writeHooksConfig() — neither accepts a scope parameter.

Files to change

  • src/hooks/hooks-config.ts — add scope-aware readScopedHooksConfig() + writeScopedHooksConfig()
  • src/hooks/manager.ts — pass scope to config write functions
  • __tests__/hooks/hooks-config.test.ts — test scope-aware writes
  • __tests__/e2e/hooks/config-scopes.e2e.test.ts — e2e test for install + scope isolation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions