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
20 changes: 20 additions & 0 deletions agentic-ai/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@rules/common/general.md
@rules/common/agents.md
@rules/common/railguard.md
@rules/bash/style.md

# Agents

Shared global instruction set for every coding agent (Claude Code, Codex,
opencode). `install.sh` symlinks this file and `rules/` into `~/.claude/`,
`~/.codex/`, and `~/` — imports resolve against the deployed directory, so both
must sit beside each instruction file.

Railguard's auto-injected guardrail block is deliberately not kept here (no
railguard start/end HTML comment markers in this file — an unpaired one would
confuse its injector). `railguard install` writes and maintains that block
per-tool, currently in `Claude/CLAUDE.md`. Duplicating it here would both
double-load for Claude, which imports this file, and go stale: the copy that
used to live here was the old long-form block, while railguard now writes a much
slimmer one. Cross-agent railguard guidance lives in
`rules/common/railguard.md`.
72 changes: 14 additions & 58 deletions agentic-ai/Claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,22 @@
@rules/common/general.md
@rules/common/agents.md
@rules/common/railguard.md
@rules/bash/style.md
@AGENTS.md

<!-- railguard:start -->
# Railguard — Active Guardrails

Railguard is monitoring this session. Every tool call (Bash, Write, Edit, Read) passes through Railguard before execution.

## What you need to know

- **Some commands will be blocked.** If you see a "denied" response from a hook, it means Railguard blocked the command. Do NOT retry the same command — find a safer alternative.
- **Some commands require human approval.** If you see an "ask" response, the human will be prompted to approve or deny.
- **File writes are snapshotted.** Every Write/Edit you make is backed up before execution. The human can rollback any change.
- **Everything is logged.** All tool calls and decisions are recorded in `.railguard/traces/`.

## Writing files

Prefer `Write`/`Edit` over Bash redirects (`cat <<EOF >`, `echo >`, `printf >`). Tool writes are snapshotted and skip the Bash path-fence scan, which matches fenced-path *strings* in command text — so a command merely mentioning a fenced path (heredoc, issue body) is blocked even if it never touches it. Switching a fence-blocked heredoc to `Write` is intended remediation, not evasion.

## If something goes wrong

If the human asks you to undo changes, fix a mistake, or rollback:
# Claude Code

1. **Get context first.** Run: `railguard context --session $SESSION_ID --verbose`
This shows you exactly what changed, diffs, blocked commands, and available rollback commands.
The section above is imported from the shared global `AGENTS.md` — the
instruction set for every agent (Claude Code, Codex, opencode). Everything there
applies. Keep cross-agent guidance in `AGENTS.md`, not here; this file is only
for things that apply to Claude Code and nothing else.

2. **Check what changed.** Run: `railguard diff --session $SESSION_ID`
Or for a specific file: `railguard diff --session $SESSION_ID --file <path>`
`@AGENTS.md` is a sibling import on purpose: Claude Code resolves `@` paths
relative to the *deployed* location of the importing file and will not follow
`../`, so `install.sh` symlinks both `AGENTS.md` and `rules/` into `~/.claude/`
next to this file.

3. **Rollback options:**
- Undo the last edit: `railguard rollback --session $SESSION_ID --steps 1`
- Undo the last N edits: `railguard rollback --session $SESSION_ID --steps N`
- Restore a specific file: `railguard rollback --session $SESSION_ID --file <path>`
- Restore everything: `railguard rollback --session $SESSION_ID`
- Restore a specific snapshot: `railguard rollback --session $SESSION_ID --id <snapshot-id>`

4. **Find your session ID.** Run: `railguard log`
This lists all sessions. Pick the most recent one.

## Configuring Railguard

You **can** help the user customize their Railguard policy. This is encouraged:

- **Read `railguard.yaml`** to understand the current policy.
- **Propose edits to `railguard.yaml`** — the user will be prompted to approve before any change is applied.
- **Run `railguard init`** to generate a starter `railguard.yaml` if one doesn't exist (user approves).
- **Run `railguard status`** to show the current protection state.
All changes to Railguard policy require explicit human approval. You generate the change, the user reviews and accepts or rejects it. Changes take effect on the next tool call — no restart needed.

### Policy layers

- **Global** `railguard.yaml` / `~/.railguard.yaml` (resolved upward from cwd) — base rules; edits gated to **ask**.
- **Per-project** `.railguard.local.yaml` (project root) — additive `fence.allowed_paths` only; cannot weaken `denied_paths` or disable the fence. Honored only if global sets `fence.allow_local_overrides: true`; the project cannot opt itself in.

Out-of-project path keeps prompting and the human wants it for this project only → add it to `.railguard.local.yaml`, not the global policy. Override ignored = global `allow_local_overrides: true` missing. Gitignore it unless the exception is shared. Details: `docs/per-project-allowlist.md`.
<!-- railguard:start -->
# Railguard - Active Guardrails

## Do NOT attempt to
Railguard monitors every tool call in this session: allow, ask, or block. If a command is blocked, do NOT re-issue it with cosmetic changes (new flags, encoding, wrappers) - take a genuinely different approach. On ask, wait for the human. File writes are snapshotted and can be rolled back.

- Run `railguard uninstall` — it will be blocked.
- Modify `~/.claude/settings.json` — it will be blocked.
- Remove the railguard binary — it will be blocked.
- Access `~/.ssh`, `~/.aws`, `~/.gnupg`, `/etc`, or other fenced paths (if path fencing is enabled).
Full agent guide (rollback commands, policy customization, path-fence quirks, self-protection): run `railguard guide`.

<!-- railguard:end -->
48 changes: 35 additions & 13 deletions agentic-ai/Claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ This will:
- Back up your existing `~/.claude/settings.json` when it differs from the template
- Copy this `settings.json` to `~/.claude/settings.json`
- Symlink `~/.claude/CLAUDE.md` → this `CLAUDE.md`
- Symlink `~/.claude/rules/` → this `rules/`
- Symlink `AGENTS.md` + `rules/` → `../AGENTS.md` and `../rules/` in
`~/.claude/`, `~/.codex/`, and `~/` (the shared cross-agent set)
- Symlink `~/.claude/docs/` → this `docs/`
- Symlink this `railguard.yaml` → `~/.railguard.yaml`
- Symlink each `hooks/*.sh` script into `~/.claude/hooks/`
Expand Down Expand Up @@ -87,22 +88,43 @@ Hooks use **exit 2** to block — Claude receives the stderr message as the reas

## Rules (Tip 6 hierarchical structure)

`CLAUDE.md` @-imports from `rules/` to keep principles modular:
`CLAUDE.md` imports `@AGENTS.md`, which imports from `rules/` — both live one
level up in `agentic-ai/`, shared with Codex and opencode:

```
rules/
common/
general.md — language-agnostic coding principles
agents.md — when to self-invoke Plan / Explore / review / verify
railguard.md — slim always-loaded Railguard behavior and reference routing
bash/
style.md — bash scripting conventions
docs/
RAILGUARD.md — expected behavior and bug-reporting protocol
per-project-allowlist.md — local path-allowlist configuration
agentic-ai/
AGENTS.md — shared cross-agent instruction set (imports the rules below)
rules/
common/
general.md — language-agnostic coding principles
agents.md — when to self-invoke Plan / Explore / review / verify
railguard.md — slim always-loaded Railguard behavior and reference routing
bash/
style.md — bash scripting conventions
Claude/
CLAUDE.md — imports @AGENTS.md, then Claude-Code-only notes
docs/
RAILGUARD.md — expected behavior and bug-reporting protocol
per-project-allowlist.md — local path-allowlist configuration
```

Add a new language by creating `rules/<lang>/style.md` and adding an `@` line to `CLAUDE.md`.
Add a new language by creating `rules/<lang>/style.md` and adding an `@` line to
`AGENTS.md` (cross-agent) or `CLAUDE.md` (Claude-only).

### Why AGENTS.md and rules/ are symlinked into three places

Claude Code resolves `@` imports against the **deployed** directory of the
importing file, and does **not** follow `../`. Verified behavior:

| Import form | Resolves? |
|---|---|
| `@rules/common/general.md` (at or below the file's dir) | yes |
| `@../AGENTS.md` (parent) | **no** |
| through a symlinked file | relative to the **symlink's** dir, not its target |

So every location holding an instruction file needs `AGENTS.md` and `rules/`
beside it. This is why the pre-existing untracked `~/AGENTS.md` was inert: its
`@rules/common/*` lines pointed at a `~/rules/` that never existed.

## Testing the hooks

Expand Down
24 changes: 19 additions & 5 deletions agentic-ai/Claude/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
set -euo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AGENTIC_DIR="$(cd "$REPO_DIR/.." && pwd)"
CLAUDE_DIR="$HOME/.claude"
HOOKS_DIR="$CLAUDE_DIR/hooks"
SETTINGS="$CLAUDE_DIR/settings.json"
Expand Down Expand Up @@ -37,11 +38,24 @@ printf 'Copied: settings.json\n'
ln -sf "$REPO_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
printf 'Linked: CLAUDE.md\n'

# Symlink rules directory (used by @imports in CLAUDE.md)
# rm -f first: ln -sf on an existing dir-symlink creates a nested link inside it
rm -f "$CLAUDE_DIR/rules"
ln -sf "$REPO_DIR/rules" "$CLAUDE_DIR/rules"
printf 'Linked: rules/\n'
# Deploy the shared cross-agent AGENTS.md + rules/ to every global location that
# reads one. Claude Code resolves @-imports against the *deployed* directory of
# the importing file and will not follow "../", so AGENTS.md and rules/ must sit
# as siblings of each instruction file — ~/AGENTS.md alone would leave its
# @rules/... imports pointing at a nonexistent ~/rules.
for agents_dir in "$CLAUDE_DIR" "$HOME/.codex" "$HOME"; do
mkdir -p "$agents_dir"
agents_dst="$agents_dir/AGENTS.md"
if [[ -e "$agents_dst" && ! -L "$agents_dst" ]]; then
backup="$agents_dst.bak.$(date +%Y%m%d%H%M%S)"
printf 'Backing up existing %s → %s\n' "$agents_dst" "$backup"
mv "$agents_dst" "$backup"
fi
ln -sf "$AGENTIC_DIR/AGENTS.md" "$agents_dst"
rm -f "$agents_dir/rules"
ln -sf "$AGENTIC_DIR/rules" "$agents_dir/rules"
printf 'Linked: AGENTS.md + rules/ → %s\n' "$agents_dir"
done

# Symlink docs directory (on-demand references pointed at by rules, e.g.
# ~/.claude/docs/RAILGUARD.md — not @imported, read only when needed)
Expand Down
10 changes: 9 additions & 1 deletion agentic-ai/Claude/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
set -uo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AGENTIC_DIR="$(cd "$REPO_DIR/.." && pwd)"
CLAUDE_DIR="$HOME/.claude"
HOOKS_DIR="$CLAUDE_DIR/hooks"
ERRORS=0
Expand Down Expand Up @@ -41,10 +42,17 @@ check_regular_file() {
section "Installed files"
check_regular_file "$CLAUDE_DIR/settings.json"
check_symlink "$CLAUDE_DIR/CLAUDE.md" "$REPO_DIR/CLAUDE.md"
check_symlink "$CLAUDE_DIR/rules" "$REPO_DIR/rules"
check_symlink "$CLAUDE_DIR/docs" "$REPO_DIR/docs"
check_symlink "$HOME/.railguard.yaml" "$REPO_DIR/railguard.yaml"

# AGENTS.md and rules/ must be siblings at every location that reads an
# instruction file: @-imports resolve against the deployed directory and don't
# follow "..", so a missing sibling silently loads nothing.
for agents_dir in "$CLAUDE_DIR" "$HOME/.codex" "$HOME"; do
check_symlink "$agents_dir/AGENTS.md" "$AGENTIC_DIR/AGENTS.md"
check_symlink "$agents_dir/rules" "$AGENTIC_DIR/rules"
done

# ── Hooks ─────────────────────────────────────────────────────────────────────
section "Hooks"
for hook in "$REPO_DIR/hooks/"*.sh; do
Expand Down
File renamed without changes.
43 changes: 38 additions & 5 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,44 @@ Format loosely follows [Keep a Changelog](https://keepachangelog.com). Remaining
work lives in [TODO.md](TODO.md); the design rationale for the unified layout is
in [UNIFICATION.md](UNIFICATION.md).

## Unreleased — config-only deploys
## Unreleased — shared global AGENTS.md

### Added
- `agentic-ai/AGENTS.md`: the tracked cross-agent global instruction set, plus
`agentic-ai/rules/` (moved up from `agentic-ai/Claude/rules/`). `install.sh`
symlinks both into `~/.claude/`, `~/.codex/`, and `~/`.

### Changed
- `agentic-ai/Claude/CLAUDE.md` slimmed to `@AGENTS.md` + Claude-only notes,
mirroring the repo-root `AGENTS.md`/`CLAUDE.md` split adopted in `f9dbe01`.

### Fixed
- The global `~/AGENTS.md` was untracked, stale, and **inert**: created 9 Jul by
railguard's installer, it missed both 12 Jul changes (`f9dbe01` AGENTS.md-as-
master, `29a2675` graphify removal), still pointed at a `~/.Codex/skills/
graphify/` that no longer exists, and its four `@rules/common/*` imports
resolved against a `~/rules/` that never existed — so none of them ever loaded.
`~/.codex/AGENTS.md` was a byte-identical copy with the same defects.

### Notes
Claude Code `@`-import semantics, verified empirically rather than assumed:
imports resolve against the **deployed** directory of the importing file, do
**not** follow `../`, and for a symlinked file resolve relative to the symlink
rather than its target. Hence `AGENTS.md` + `rules/` are symlinked as siblings at
every location that holds an instruction file, and `validate.sh` now checks all
six links.

## 2026-07-28 — Config-only deploys ([#65](https://github.com/ulises-c/Computer-Setup/pull/65))

### Added
- `setup.sh --dotfiles`: deploys only the shared dotfiles set (`~/.zshrc`,
`~/.tmux.conf`, `~/.zsh_plugins.txt`, `~/.p10k.zsh`) and installs no packages.
Short-circuits before tag validation and the interactive prompt, so selection
flags don't apply. Combine with `--dry-run` to preview.
- `fixterm` alias in `dotfiles/zshrc.example` — resets stuck mouse (1000/1002/
1003/1006/1015) and focus (1004) reporting after an app dies without cleanup,
a common sleep/wake symptom. Cheaper than `reset`, which also clears scrollback.
- `fixterm` alias in `dotfiles/zshrc.example` — resets stuck mouse reporting
(modes 9/1000/1001/1002/1003/1005/1006/1015/1016) and focus reporting (1004)
after an app dies without cleanup, a common sleep/wake symptom. Cheaper than
`reset`, which also clears scrollback.

### Changed
- The four dotfile deploys, previously duplicated between `platforms/macos.sh`
Expand All @@ -28,9 +56,14 @@ in [UNIFICATION.md](UNIFICATION.md).
- `driftcheck.sh` Stop hook aborted with `ignore_patterns[@]: unbound variable`
on every session end when neither ignore file existed — macOS bash 3.2 treats
`"${arr[@]}"` on an empty array as unset under `set -u`. It exited 1 (generic
hook error) instead of the intended 2, masking real violations.
hook error) instead of the intended 2, masking real violations. Two further
exit-2 gaps found during review are tracked in
[#67](https://github.com/ulises-c/Computer-Setup/issues/67).
- `benchmarking/lib/common.sh` was mode 644 with a shebang while its siblings
`lib/core.sh` and `lib/verify.sh` are 755 — the violation the crash was hiding.
- `CONFIG_SRC_DIR` is now assigned unconditionally on the `--dotfiles` path
(`core_resolve_config_src_dir`), so an inherited environment value can't decide
which `zshrc.example` wins.
- Stale comment in `lib/core.sh` claiming `linux-server` ships a `zshrc.example`
override; no platform does.

Expand Down
Loading