chore(agent): capture noctalia/gate/niri memory lessons + secret-audit and system-prompt-retrieval managed skills
learned.md: noctalia v5 bar layering + hot-reload loop; secret-gate self-match convention (quoted literals redacted per the convention the line itself states); niri 26.04 event-stream has no hotplug event. New managed skills: git-repo-secret-audit, omp-system-prompt-retrieval.
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
name: git-repo-secret-audit
|
||||||
|
description: "Audit a git repo for leaked secrets across history, unreachable objects, LFS payloads, git config and host configs — with triage of every hit."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git repo secret audit (history + unreachable objects, not just the worktree)
|
||||||
|
|
||||||
|
Worktree greps prove nothing: the leak you care about is in a commit that was amended
|
||||||
|
away, in a branch nobody checks out, in an LFS payload, or in `.git/config`.
|
||||||
|
|
||||||
|
## 0. Build the known-value set from the live secrets, never from literals
|
||||||
|
Read the values from the secret store at runtime so no key enters the transcript:
|
||||||
|
```python
|
||||||
|
known = {}
|
||||||
|
for p in (home/".config/secrets").iterdir():
|
||||||
|
t = p.read_text().strip()
|
||||||
|
vals = {t}
|
||||||
|
m = re.search(r":([^:@/]+)@", t) # credential-store URL form
|
||||||
|
if m: vals.add(m.group(1))
|
||||||
|
known[p.name] = {v for v in vals if len(v) >= 12}
|
||||||
|
```
|
||||||
|
Report ONLY `value[:4] + len + sha256[:10]`. Never print a value, and never quote a
|
||||||
|
secret-shaped literal in a doc/commit message — the pre-commit gate will (correctly) block it.
|
||||||
|
|
||||||
|
## 1. Shapes to match (superset of the Dank gate)
|
||||||
|
`sk-[A-Za-z0-9]{18,}`, `sk-or-v1-`, `sk-ant-`, `hf_[A-Za-z0-9]{30,}`, `gh[pousr]_[A-Za-z0-9]{30,}`,
|
||||||
|
`github_pat_`, `glpat-`, `xox[baprs]-`, `(AKIA|ASIA)[0-9A-Z]{16}`, `npm_`, `pypi-`, `AIza[0-9A-Za-z_-]{30,}`,
|
||||||
|
`(sk|pk)_(live|test)_`, Telegram `\d{8,10}:AA`, JWT `eyJ…\.eyJ…`, `-----BEGIN [A-Z ]*PRIVATE KEY-----`,
|
||||||
|
`AGE-SECRET-KEY-1`, `ssh-(ed25519|rsa)\s+AAAA`, `scheme://user:pass@host` basic-auth URLs,
|
||||||
|
`Bearer <24+>`, and the noisy `(password|secret|api_key|token)\s*[:=]\s*<12+>` assignment shape.
|
||||||
|
|
||||||
|
## 2. Scope, in this order
|
||||||
|
```bash
|
||||||
|
git rev-list --all | wc -l # revs incl. every branch + tag
|
||||||
|
git ls-tree -r --format='%(objectname) %(path)' <rev> # sha -> path, per rev
|
||||||
|
git cat-file --batch # stream reachable blob content
|
||||||
|
git cat-file --batch-all-objects --batch-check # EVERY object, incl. dangling
|
||||||
|
git rev-list --objects --all # the reachable set; difference = dangling
|
||||||
|
git log --all --oneline --find-object=<sha> # empty => never committed on any ref
|
||||||
|
git status --porcelain -uall --ignored=matching # !! lines: on-disk ignored files
|
||||||
|
git config --list --show-origin # local/global/system credentials
|
||||||
|
git ls-files | grep -E '(auth\.json|trust\.json|\.env|\.pem|\.key|identity)' # quarantine
|
||||||
|
```
|
||||||
|
LFS: scan `.git/lfs/objects/**` as BYTES (`decode("latin-1")`) — a screenshot is a classic
|
||||||
|
secret carrier and the tracked tree holds only pointers.
|
||||||
|
|
||||||
|
## 3. Entropy layer (catches issuer-specific keys you did not list)
|
||||||
|
Tokens `[A-Za-z0-9+/_-]{32,}` with Shannon entropy >= 4.3 containing both digits and letters.
|
||||||
|
Flag every one whose value is in `known` as a LIVE leak; list the rest for triage.
|
||||||
|
|
||||||
|
## 4. Triage — the shapes that always fire and are fine
|
||||||
|
- `api_key="sk-no-key-required"` and other doc placeholders
|
||||||
|
- prose: `Set password: user.set_password(...)`, `authenticate(user, password)`
|
||||||
|
- 40-hex hits that are git object ids / packfile names (`pack-<40hex>.pack`)
|
||||||
|
- memory/KB records that *describe* a secret operation
|
||||||
|
A hit is a leak only when the value matches a live secret, decodes to a real credential, or
|
||||||
|
sits in a field a program reads.
|
||||||
|
|
||||||
|
## 5. Adjacent host scan (the audit's real yield)
|
||||||
|
Check file modes of the secret dir (`0600`, dir `0700`) — world-readable secret files are the
|
||||||
|
most common real finding. Then grep the live values across shell rc files, `~/.gitconfig`,
|
||||||
|
`~/.npmrc`, `~/.netrc`, `~/.curlrc`, `~/.docker/config.json`, `~/.cache/huggingface/token`,
|
||||||
|
`~/.aws/credentials`, `~/.ssh/config` and report filenames only.
|
||||||
|
|
||||||
|
## 6. Report
|
||||||
|
Per layer: scope numbers + verdict. Every hit: path:line, redacted value, verdict
|
||||||
|
(leak / benign + why). Name the gate's coverage gap if the project's own hook misses shapes
|
||||||
|
you matched. Fix modes/chmod on the spot; changes to a security hook are the operator's call.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: omp-system-prompt-retrieval
|
||||||
|
description: "Retrieve the exact system prompt an omp session is running: /dump while live, RPC get_state re-render, and the shipped template/override source paths."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Retrieve omp's live system prompt
|
||||||
|
|
||||||
|
Three routes, in fidelity order. Verified on omp 18.4.10 (linux/x64).
|
||||||
|
|
||||||
|
## 1. `/dump` — the only exact capture of a LIVE session
|
||||||
|
TUI slash command: "Copy session transcript to clipboard (and write LLM request JSON to tmp)".
|
||||||
|
The request JSON is the wire-true payload (system blocks + tool schemas). Path is printed in
|
||||||
|
the `/dump` output; then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jq -r '.system[] | strings // .system' <file> # anthropic shape
|
||||||
|
jq -r '.messages[] | select(.role=="system") | .content' <file> # openai shape
|
||||||
|
```
|
||||||
|
|
||||||
|
**The prompt is never persisted in the session JSONL.** Session entries are only
|
||||||
|
`title, session, model_change, thinking_level_change, message, custom(tool_execution_start),
|
||||||
|
model_usage`. So a past session's prompt is unrecoverable from disk — capture live, or re-render.
|
||||||
|
|
||||||
|
## 2. RPC `get_state` — re-render without a model call
|
||||||
|
`omp --mode rpc` answers `{"type":"get_state"}` with `data.systemPrompt: [blocks]` and
|
||||||
|
`data.dumpTools` (spec: `omp://rpc.md` "get_state payload"). Drive it over stdio:
|
||||||
|
|
||||||
|
```python
|
||||||
|
cmds = json.dumps({"id":"p1","type":"negotiate_protocol","protocolVersion":2}) + "\n" \
|
||||||
|
+ json.dumps({"id":"s1","type":"get_state"}) + "\n"
|
||||||
|
p = subprocess.run(["omp","--mode","rpc","--no-session","--no-pty"],
|
||||||
|
cwd=TARGET_CWD, input=cmds, capture_output=True, text=True, timeout=280)
|
||||||
|
```
|
||||||
|
|
||||||
|
Closing stdin shuts it down cleanly (exit 0), no model call, no session file written.
|
||||||
|
|
||||||
|
- Parse stdout line-by-line. Protocol v2 wraps logical frames >1 MiB in `rpc_chunk` frames:
|
||||||
|
key on `chunkId`, require all `count` indices, `base64.b64decode` and concatenate in `index`
|
||||||
|
order, then `json.loads` — otherwise a large prompt is silently lost.
|
||||||
|
- Shape measured on a dotfiles repo cwd: 2 blocks, ~45 KB / ~391 lines —
|
||||||
|
block 0 = static instruction block (RFC 2119 line, `§ Role`, Engineering/Tone/Escalation,
|
||||||
|
tool inventory, `xd://` catalog), block 1 = footer opening `<project-context>` +
|
||||||
|
`<critical>` + the cwd's `AGENTS.md`.
|
||||||
|
- `--no-extensions` changes nothing in the blocks; tool count rides in `dumpTools`, not the prompt.
|
||||||
|
- `cwd` matters: the footer is cwd-derived (workstation block, context files, skills, rules).
|
||||||
|
|
||||||
|
## 3. Sources (edit here, not in a dump)
|
||||||
|
| Layer | Path |
|
||||||
|
|---|---|
|
||||||
|
| Default instruction template (Handlebars, block 0) | `<pkg>/src/prompts/system/system-prompt.md` |
|
||||||
|
| Footer | `<pkg>/src/prompts/system/project-prompt.md` |
|
||||||
|
| Plain-custom template | `<pkg>/src/prompts/system/custom-system-prompt.md` |
|
||||||
|
| Repo rules (into footer) | the cwd's `AGENTS.md` |
|
||||||
|
| Overrides | `--system-prompt`, `--system-prompt-template <path>`, `--append-system-prompt`; discovered `SYSTEM.md`, `SYSTEM_TEMPLATE.md`, `APPEND_SYSTEM.md`, `PERSONALITY.md`, `TITLE_SYSTEM.md` |
|
||||||
|
|
||||||
|
`<pkg>` = `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent` (resolve via
|
||||||
|
`readlink -f $(command -v omp)`). Docs: `omp://system-prompt-customization.md`.
|
||||||
|
|
||||||
|
Precedence: explicit CLI flag > project `SYSTEM.md` > project `SYSTEM_TEMPLATE.md` >
|
||||||
|
user `SYSTEM.md` > user `SYSTEM_TEMPLATE.md`. A same-scope literal always beats a discovered
|
||||||
|
template, so a `SYSTEM_TEMPLATE.md` is inert while a `SYSTEM.md` sits beside it.
|
||||||
|
|
||||||
|
## Trap
|
||||||
|
NEVER paste a rendered `/dump` into `SYSTEM_TEMPLATE.md`: a render freezes settings, tool
|
||||||
|
catalogs, and mounted-device data. Copy the Handlebars **source** and diff against the shipped
|
||||||
|
path after every omp upgrade. Plain files (`SYSTEM.md`, `APPEND_SYSTEM.md`) are inserted
|
||||||
|
verbatim — `{{cwd}}` and friends stay literal there; only the template route compiles Handlebars.
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
- Noctalia v5 config layering (verified live on blankpad 2026-10-09): bar widget layout lives in the GUI-override layer ~/.local/state/noctalia/settings.toml under [bar.name] keys start/center/end (defaults: start=["launcher","wallpaper","workspaces"], center=["clock"]); settings.toml loads LAST and wins over ~/.config/noctalia/config.toml. Both layers are watched and hot-reloaded — edit + noctalia config validate + screenshot is the full apply/verify loop (no restart, no reload IPC). Noctalia writes THROUGH symlinks and keeps the link, so keeping settings.toml symlinked from the command-center dotfiles repo is safe. Combine workspace+window indicator: [widget.taskbar] group_by_workspace=true (needs compositor per-window workspace data — niri has it) renders per-workspace capsules with disc labels, workspace_group_content = icons|count|dots (count/dots left-click activates the workspace); the workspaces widget itself has no window-display mode. Applied on blankpad: bar.default start=["launcher","wallpaper","workspaces","taskbar"], center=["cat"] (command-center commits 9255a8b, 0ccdd46). _(context: Blankpad noctalia 5.2.1 bar layout change 2026-10-09: taskbar moved to left section, group_by_workspace identified as the combining widget, verified via config validate + fullscreen screenshot)_
|
||||||
|
- The command-center (~/.omp) pre-commit secret gate (.githooks/pre-commit, single grep -E of known token regexes) SELF-MATCHES its own documented pattern literals: committing prose that quotes them verbatim (e.g. a quoted PEM private-key header line, the xox[baprs] family prefix, the github-pat prefix, long sk- examples) trips SECRET GATE and blocks the commit (observed twice 2026-10-09 on agent/memories/--home-blank-.omp--/learned.md audit prose). Convention: redact pattern literals descriptively in prose ("PEM private-key headers", "the xox[baprs] family", "ghp 36") instead of setting ALLOW_SECRETS=1; the gate firing on prose is the gate working as designed. Note the gate covers only 10 families — no hf_/npm_/AIza/glpat-short/basic-auth-URL coverage — so clean-gate history is not proof of no leaks.
|
||||||
|
- niri 26.04 event-stream (niri msg --json event-stream) has NO hotplug event: OutputConfigChanged is only an ACK that an IPC/config output change was Applied (per niri-ipc docs; proven live — mode flips via niri msg output NAME mode WxH@Hz and DPMS off/on emit zero output events). Detect monitor connect/disconnect by diffing the sorted unique set of .WorkspacesChanged.workspaces[].output across events (off/on cycle of eDP-1 emitted exactly WorkspacesChanged 2x). Debounce ~1.5s; OutputConfigChanged events are emitted in bursts on every config reload — ignoring them prevents hotplug-daemon echo loops. Implemented in dank/command-center scripts/desk-autoswitch.sh (blankpad-desk-autoswitch.service). Related trap: niri msg output 26.04 syntax is niri msg output NAME ACTION [args] (mode/off/on), not the old NAME WxH@Hz form.
|
||||||
- On Frank's workstations (Arch, pam_faillock deny=3), subagents MUST NEVER run sudo/yay/pacman: in the harness sudo has no TTY, so every attempt logs 3 pam_unix failures and trips pam_faillock "temporarily locked out" (~600s), which re-arms on every retry and locks out the USER's real interactive sudo — observed 2026-10-09 on blankpad when a Handy-install subagent looped sudo yay -Syu handy-bin for 15 min; the user saw "incorrect password"/lockout and (understandably) suspected his password was changed. Diagnostics that settle it in one step: stat /etc/shadow (unchanged mtime = no password change), journalctl | grep 'sudo\[' (PWD/COMMAND identify the spammer), faillock --user blank. Recovery: kill the loop, then faillock --user self --reset works unprivileged (per-user dir /run/faillock/user is owner-writable); then user runs sudo -v. Any package install requiring root must be handed to the user as exact copy-paste commands, never attempted by an agent.
|
- On Frank's workstations (Arch, pam_faillock deny=3), subagents MUST NEVER run sudo/yay/pacman: in the harness sudo has no TTY, so every attempt logs 3 pam_unix failures and trips pam_faillock "temporarily locked out" (~600s), which re-arms on every retry and locks out the USER's real interactive sudo — observed 2026-10-09 on blankpad when a Handy-install subagent looped sudo yay -Syu handy-bin for 15 min; the user saw "incorrect password"/lockout and (understandably) suspected his password was changed. Diagnostics that settle it in one step: stat /etc/shadow (unchanged mtime = no password change), journalctl | grep 'sudo\[' (PWD/COMMAND identify the spammer), faillock --user blank. Recovery: kill the loop, then faillock --user self --reset works unprivileged (per-user dir /run/faillock/user is owner-writable); then user runs sudo -v. Any package install requiring root must be handed to the user as exact copy-paste commands, never attempted by an agent.
|
||||||
- Tool traps verified on blankpad 2026-10-09 (niri/noctalia/Handy DE session): (1) niri msg action (and other niri msg subcommands that print help/action lists) write to STDERR — niri msg action 2/dev/null | grep ... yields empty output and looks like "feature missing"; always pipe with 2&1. Confirmed live: niri 26.04 has swap-window-left/right (window walks sideways, entering/leaving columns; column order untouched), move-window-to-monitor-*, focus-window-up/down (tabbed columns navigate with up/down; focus-window-left/right does NOT exist — no gap to fill). (2) frank-zealot (llamacpp dankclaw-3090 :7771) as a subagent model crashed mid-run with invalid string: surrogate U+D800..U+DBFF when a tool-call JSON argument contained many emoji — server-side JSON parse, type=server_error; keep subagent prompt/context payloads ASCII (no emoji) when agents run on fleet llama.cpp/radiance models; the crash is infrastructure, retry after stripping emoji. (3) The failed agent HAD already written its files before crashing (omp-docs/tools/handy.md, noctalia-shell.md rewrite survived; only its cleanup grep died) — after a subagent exit-1, always inspect target files for partial/complete output before respawning; its summary claims are unverified, diff against live truth (it invented a Handy settings JSON shape and flipped post_process_enabled true; live value false, real shape has bindings.transcribe/current_binding). (4) edit tool payload format: every change block MUST start with a PUT N.=M: (or DEL N.=M) hunk header followed by +/- lines; bare N:text + +text lines are rejected with "payload line has no preceding hunk header". (5) blankpad repo fact for pending work: live ~/.config/niri/cfg/{misc,rules}.kdl symlinks point at the GENERIC dotfiles/niri copies, which pin render-node /dev/dri/renderD129 "Intel Arc" — blankpad is single AMD Cezanne (renderD128); the newer dotfiles/zz-blankpad copies (no render-node, cursor 36, blur off, noctalia layer rules) are the correct live files, _(context: Grilling session on blankpad niri 26.04 / noctalia 5.2.1 / Handy 0.9.6 DE config; fact-check sweep + doc rewrites in dank/command-center omp-docs.)_
|
- Tool traps verified on blankpad 2026-10-09 (niri/noctalia/Handy DE session): (1) niri msg action (and other niri msg subcommands that print help/action lists) write to STDERR — niri msg action 2/dev/null | grep ... yields empty output and looks like "feature missing"; always pipe with 2&1. Confirmed live: niri 26.04 has swap-window-left/right (window walks sideways, entering/leaving columns; column order untouched), move-window-to-monitor-*, focus-window-up/down (tabbed columns navigate with up/down; focus-window-left/right does NOT exist — no gap to fill). (2) frank-zealot (llamacpp dankclaw-3090 :7771) as a subagent model crashed mid-run with invalid string: surrogate U+D800..U+DBFF when a tool-call JSON argument contained many emoji — server-side JSON parse, type=server_error; keep subagent prompt/context payloads ASCII (no emoji) when agents run on fleet llama.cpp/radiance models; the crash is infrastructure, retry after stripping emoji. (3) The failed agent HAD already written its files before crashing (omp-docs/tools/handy.md, noctalia-shell.md rewrite survived; only its cleanup grep died) — after a subagent exit-1, always inspect target files for partial/complete output before respawning; its summary claims are unverified, diff against live truth (it invented a Handy settings JSON shape and flipped post_process_enabled true; live value false, real shape has bindings.transcribe/current_binding). (4) edit tool payload format: every change block MUST start with a PUT N.=M: (or DEL N.=M) hunk header followed by +/- lines; bare N:text + +text lines are rejected with "payload line has no preceding hunk header". (5) blankpad repo fact for pending work: live ~/.config/niri/cfg/{misc,rules}.kdl symlinks point at the GENERIC dotfiles/niri copies, which pin render-node /dev/dri/renderD129 "Intel Arc" — blankpad is single AMD Cezanne (renderD128); the newer dotfiles/zz-blankpad copies (no render-node, cursor 36, blur off, noctalia layer rules) are the correct live files, _(context: Grilling session on blankpad niri 26.04 / noctalia 5.2.1 / Handy 0.9.6 DE config; fact-check sweep + doc rewrites in dank/command-center omp-docs.)_
|
||||||
- Blankpad DE stack, verified 2026-10-09 against live system + upstream (source of truth for future niri/noctalia/handy work; supersedes omp-docs claims): (1) Noctalia is v5 (installed 5.2.1-1.1, native noctalia binary). The v4 Quickshell line is DEAD upstream — repo doc omp-docs/tools/noctalia-shell.md (v4.7.7, qs -c noctalia-shell ipc call …) is stale; correct API is noctalia msg cmd, full list from noctalia msg --help (~110 cmds: screenshot-region/-fullscreen/-annotate, window-switcher, clipboard-*, workspace-alert-*, theme-mode-toggle, session lock-and-suspend, dpms-*, greeter-sync, templates-apply). Noctalia's niri docs (docs.noctalia.dev/noctalia/compositor-settings/niri/): autostart spawn-at-startup "noctalia", lid policy via niri switch-events { lid-close { spawn "noctalia" "msg" "session" "lock-and-suspend"; } }, wallpaper/blur layer-rules, [shell] niri_overview_type_to_launch_enabled. (2) niri 26.04 IS latest (repo moved to niri-wm org). New since docs: mainline blur/xray (background-effect, popups block, layer matcher), include optional=true + ~ expansion, niri msg action load-config-file --path file (live-swap config path — key for dotfiles symlink testing), cast IPC (niri msg casts, stop-cast), screenshot-window show-pointer=true, output options variable-refresh-rate (+on-demand=true), focus-at-startup, backdrop-color, per-output layout{}, mode custom=true/modeline (25.11), Escape cancels drag-and-drop. (3) Handy (cjpais/Handy) 0.9.6 installed via local/handy-bin; latest 0.9.8 (2026-10-03). Upstream LAW: SIGUSR1 handler REMOVED (WebKitGTK owns SIGUSR1; pkill -USR1 handy can crash the app) — toggle is SIGUSR2 or CLI flags handy --toggle-transcription / --toggle-post-process / --cancel; startup flags --start-hidden --no-tray. Wayland: compositor must own global binds (call the CLI flags from niri binds); text input needs wtype/dotool (NOT installed on blankpad; current paste_method=none + copy_to_clipboard, so no tool needed); overlay disabled-by-default up _(context: Fact-check sweep of niri/noctalia/handy docs on blankpad, 2026-10-09, ahead of a pending config-change grilling (Round 1 questions pending user answers))_
|
- Blankpad DE stack, verified 2026-10-09 against live system + upstream (source of truth for future niri/noctalia/handy work; supersedes omp-docs claims): (1) Noctalia is v5 (installed 5.2.1-1.1, native noctalia binary). The v4 Quickshell line is DEAD upstream — repo doc omp-docs/tools/noctalia-shell.md (v4.7.7, qs -c noctalia-shell ipc call …) is stale; correct API is noctalia msg cmd, full list from noctalia msg --help (~110 cmds: screenshot-region/-fullscreen/-annotate, window-switcher, clipboard-*, workspace-alert-*, theme-mode-toggle, session lock-and-suspend, dpms-*, greeter-sync, templates-apply). Noctalia's niri docs (docs.noctalia.dev/noctalia/compositor-settings/niri/): autostart spawn-at-startup "noctalia", lid policy via niri switch-events { lid-close { spawn "noctalia" "msg" "session" "lock-and-suspend"; } }, wallpaper/blur layer-rules, [shell] niri_overview_type_to_launch_enabled. (2) niri 26.04 IS latest (repo moved to niri-wm org). New since docs: mainline blur/xray (background-effect, popups block, layer matcher), include optional=true + ~ expansion, niri msg action load-config-file --path file (live-swap config path — key for dotfiles symlink testing), cast IPC (niri msg casts, stop-cast), screenshot-window show-pointer=true, output options variable-refresh-rate (+on-demand=true), focus-at-startup, backdrop-color, per-output layout{}, mode custom=true/modeline (25.11), Escape cancels drag-and-drop. (3) Handy (cjpais/Handy) 0.9.6 installed via local/handy-bin; latest 0.9.8 (2026-10-03). Upstream LAW: SIGUSR1 handler REMOVED (WebKitGTK owns SIGUSR1; pkill -USR1 handy can crash the app) — toggle is SIGUSR2 or CLI flags handy --toggle-transcription / --toggle-post-process / --cancel; startup flags --start-hidden --no-tray. Wayland: compositor must own global binds (call the CLI flags from niri binds); text input needs wtype/dotool (NOT installed on blankpad; current paste_method=none + copy_to_clipboard, so no tool needed); overlay disabled-by-default up _(context: Fact-check sweep of niri/noctalia/handy docs on blankpad, 2026-10-09, ahead of a pending config-change grilling (Round 1 questions pending user answers))_
|
||||||
|
|||||||
Reference in New Issue
Block a user