Skip to content

chore(policy): remove employer name from current files; hashed employer-data gate - #27

Merged
Jott2121 merged 5 commits into
mainfrom
cursor/remove-employer-name-7006
Oct 11, 2026
Merged

Jott2121 merged 5 commits into
mainfrom
cursor/remove-employer-name-7006

Conversation

@Jott2121

@Jott2121 Jott2121 commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

This removes an employer reference from the current files. Older commits still contain it; a history rewrite is out of scope by the owner's choice.

What

  • Replaced the employer's name with generic wording in the five files where it appeared: AGENTS.md, .cursor/rules/00-core-principles.mdc, .cursor/BUGBOT.md, .cursor/hooks/format-and-scan.sh, .github/workflows/ci.yml. All five came from the setup PR (chore: add Cursor agent setup and receipt checks #26), which is already merged. New wording: never add anything from an employer's systems, workforce data, internal URLs, documents, or proprietary/export-controlled material.
  • Kept the no-employer-data gate working and fail-closed without the literal name in the repo:
    • .github/blocked-terms.sha256 stores only SHA-256 hashes of the normalized blocked terms (lowercase words joined by single spaces), each followed by its word count. There are 4 entries: the name, the two-word name, the concatenated form, and the abbreviation.
    • scripts/scan_blocked_terms.py (stdlib only) hashes every word and adjacent-word phrase in every tracked file and file path, and fails on any match. It reports a hit as path:line plus a hash prefix, never the matched text.
    • Optional EMPLOYER_MARKERS env var / Actions secret: a regex alternation matched case-insensitively on top of the hashes. It is additive only. With no secret set, the hash list still catches the name.
    • Fails closed (exit 2) if the hash file is missing, empty, or malformed, if EMPLOYER_MARKERS doesn't compile, or if a target can't be read.
    • The export-control markers from the old pattern (controlled-information, ITAR-style, "export controlled", and uppercase proprietary) are kept as readable regexes. They are written so the scanner never matches its own source.
  • Scan coverage: the old job excluded .cursor/, AGENTS.md, and .github/, and dropped the author-byline lines. All of that is gone. Now the whole tracked tree is scanned, including experiments/, paper/, fixtures, AGENTS.md, .cursor/, and .github/. The only exclusions are the hash file itself and generated junk dirs (.git, node_modules, build, dist, __pycache__, .venv, mutants). The test fixtures are created at runtime in temp dirs and are never tracked.
  • format-and-scan.sh now calls the same scanner. A scanner error also logs a warning. The secret-key patterns are unchanged.
  • verify-receipts step 3 now runs the scanner. BUGBOT now treats loosening the hash file or scanner as blocking.
  • pyproject.toml: added --ignore=tests/test_scan_blocked_terms.py to the mutmut test selection. The test reads .github/ and scripts/, which aren't in mutmut's sandbox. Same precedent as test_guard_shell.py; the normal pytest jobs still run it.

Employer-name check

  • The employer-name grep (before: 5 hits, after: 0). Before: one line in each of the five files above, on main at b99e4a6. After, on this branch: empty output.
  • Whole-word hits for the two-letter abbreviation (3 before):
    • .cursor/BUGBOT.md:9: the employer abbreviation, in a list of blocked names. Removed.
    • docs/assets/demo.gif, paper/crucible-paper.pdf: binary matches only. They are those two bytes occurring inside compressed image/stream data. Text extracted from both paper PDFs with pypdf has no employer reference.
    • After: no text hits. "LLM", "LMS", and similar tokens were never touched. The scanner doesn't hash the bare two-letter abbreviation, because it would false-positive on ordinary uses of the same two letters, such as shorthand for "language model". Add it as a hash later if wanted.
  • Other pointers to the employer (programs, products, sites, internal acronyms, role titles that name the company): none found. I checked the saved AI review answers in paper/*.txt, the paper drafts and PDFs, experiments/, docs, and fixtures. The paper bylines read "(Independent)" or a university affiliation, which is not the employer, so they were left as is.

How verified

Scanner tests (python -m pytest tests/test_scan_blocked_terms.py -v): 18 passed. Highlights:

  • test_clean_tree_passes: whole repo, exit 0.
  • test_hashed_word_planted_in_temp_file_is_caught, test_hashed_two_word_phrase_matches_across_punctuation_and_lines: terms planted in temp files, caught, and not echoed.
  • test_employer_markers_regex_adds_terms_without_echoing_them, test_unset_employer_markers_still_enforces_hashes.
  • test_whole_tree_mode_scans_data_policy_and_config_folders_and_paths: hits in results/, fixtures/ (path), AGENTS.md, .cursor/, .github/.
  • test_hash_file_itself_is_not_scanned, test_generated_junk_dirs_are_skipped, test_export_control_marker_is_caught, test_whole_words_only.
  • Fail-closed: missing / empty (even with EMPLOYER_MARKERS set) / malformed hash file, invalid regex, and unreadable target all exit 2.
  • test_scanner_source_needs_no_self_exemption, test_ci_runs_scanner_with_optional_secret.

Against the real committed hashes, I planted each form of the actual name in /tmp/plant.txt (outside the repo) with no secret set:

scan_blocked_terms: 1 hit(s) in 1 file(s)
/tmp/plant.txt:2: blocked term (sha256 cb5c36fe5905)     # name, any case
/tmp/plant.txt:2: blocked term (sha256 c37601352284)     # two-word name, also hyphenated
/tmp/plant.txt:2: blocked term (sha256 f277af9c6419)     # concatenated / domain form
/tmp/plant.txt:2: blocked term (sha256 8c6a10a10f5d)     # abbreviation

Each run exited 1. A file with LM Studio and LLM and LMS exits 0. Clean tree: scan_blocked_terms: clean (799 files, 4 hashed terms), exit 0.

CI on 115f887, all green:

Job Status
ci / test (3.11) pass
ci / test (3.12) pass
ci / test (3.13) pass
ci / receipts pass
ci / no-employer-data pass (log: scan_blocked_terms: clean (799 files, 4 hashed terms))
mutation / score pass (99%, 1165/1171 killed, same as before)
CodeQL / Analyze (python) pass
CodeQL pass

verify-receipts result (.cursor/skills/verify-receipts/SKILL.md)

  • ruff check src tests: pass (exit 0)
  • python -m pytest -q -m "not slow" --cov --cov-report=term-missing --cov-fail-under=90: pass. 476 passed, 9 deselected, coverage 95.54% (run at 418bc0a; the only later change is the mutmut ignore line in pyproject.toml).
  • python -m pytest -q -m slow: pass. 9 passed, 472 deselected.
  • python scripts/check_receipts.py: receipts ok (H1 b=105 c=0 p=4.9×10⁻³²; E-B1 0.783 [0.592, 0.935]; E-B2 0.178 [0.039, 0.347]).
  • python3 scripts/scan_blocked_terms.py: clean, exit 0.
  • Numbers changed in README / docs / RESULTS / paper: none.
  • Thresholds: --cov-fail-under 90, pyproject fail_under 90, and mutation fail-under "95" are all unchanged. No tests skipped or removed.
  • Not verified / known gaps: listed below.

Numbers changed

None. The README badge and AGENTS.md say 462 tests, but main already collects 467, so that was stale before this PR. This PR adds 18 (485 total). I left the count as is to keep this PR to one intent.

Risks / limits

  • Matching is whole-word and whole-phrase. A term glued inside a larger token (e.g. xNAMEy) is not caught. The concatenated two-word form is hashed separately for that reason.
  • Hashes of short dictionary words can be reversed by brute force. The goal is no plaintext name in the repo, not secrecy.
  • Binary files (NUL in the first 8 KB) are skipped, same as the old git grep -I.
  • EMPLOYER_MARKERS is optional. If the owner wants extra terms, add an Actions secret named EMPLOYER_MARKERS. Nothing is required for this PR to be green.

Not done

Open in Web Open in Cursor 

cursoragent and others added 5 commits October 11, 2026 16:44
Blocked terms are committed only as SHA-256 hashes; the scanner hashes every
word and adjacent-word phrase in tracked files and paths, honors an optional
BLOCKED_TERMS env var, skips only generated junk dirs, and exits 2 when the
hash file is missing, empty, or malformed.

Co-authored-by: Jeff Otterson <Jott2121@users.noreply.github.com>
…ta wording

Co-authored-by: Jeff Otterson <Jott2121@users.noreply.github.com>
Drops the literal pattern and the path/byline exclusions; the edit hook calls
the same scanner.

Co-authored-by: Jeff Otterson <Jott2121@users.noreply.github.com>
…mption

The committed hash list stays the fail-closed base. EMPLOYER_MARKERS adds a
case-insensitive regex alternation on top; an invalid one fails the scan. The
only exclusions are now the hash file and generated junk dirs.

Co-authored-by: Jeff Otterson <Jott2121@users.noreply.github.com>
It reads .github/ and scripts/, which mutmut does not copy; the normal pytest
jobs still run it, same as test_guard_shell.py.

Co-authored-by: Jeff Otterson <Jott2121@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

🧪 Mutation score: 99%

Crucible injected 1171 real defects into [tool.mutmut] scope. This suite killed 1165 of them.

6 defects survived — real bugs this suite would not catch.

Show the survivors
crucible.loop.x__run__mutmut_46
crucible.guardrails.x_extract_test_file__mutmut_4
crucible.guardrails.x__parse_failed_test_names__mutmut_3
crucible.report.x_mcnemar_exact__mutmut_4
crucible.scope.x_canary_probe__mutmut_125
crucible.lean.x__build_argv__mutmut_3

Coverage measures what ran. This measures what would be caught. crucible · no model, no API key, $0

@Jott2121
Jott2121 marked this pull request as ready for review October 11, 2026 16:59
@Jott2121
Jott2121 merged commit 8bab2d1 into main Oct 11, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants