paper-trail-gem/paper_trail
Track changes to your rails models
Healthy across the board
Permissive license, no critical CVEs, actively maintained — safe to depend on.
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
No critical CVEs, sane security posture — runnable as-is.
- ✓Last commit 1d ago
- ✓29+ active contributors
- ✓Distributed ownership (top contributor 48% of recent commits)
Show 3 more →Show less
- ✓MIT licensed
- ✓CI configured
- ✓Tests present
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Healthy" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/paper-trail-gem/paper_trail)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/paper-trail-gem/paper_trail on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: paper-trail-gem/paper_trail
Generated by RepoPilot · 2026-05-10 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/paper-trail-gem/paper_trail shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
GO — Healthy across the board
- Last commit 1d ago
- 29+ active contributors
- Distributed ownership (top contributor 48% of recent commits)
- MIT licensed
- CI configured
- Tests present
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live paper-trail-gem/paper_trail
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/paper-trail-gem/paper_trail.
What it runs against: a local clone of paper-trail-gem/paper_trail — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in paper-trail-gem/paper_trail | Confirms the artifact applies here, not a fork |
| 2 | License is still MIT | Catches relicense before you depend on it |
| 3 | Default branch master exists | Catches branch renames |
| 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code |
| 5 | Last commit ≤ 31 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of paper-trail-gem/paper_trail. If you don't
# have one yet, run these first:
#
# git clone https://github.com/paper-trail-gem/paper_trail.git
# cd paper_trail
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of paper-trail-gem/paper_trail and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "paper-trail-gem/paper_trail(\\.git)?\\b" \\
&& ok "origin remote is paper-trail-gem/paper_trail" \\
|| miss "origin remote is not paper-trail-gem/paper_trail (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
&& ok "license is MIT" \\
|| miss "license drift — was MIT at generation time"
# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
&& ok "default branch master exists" \\
|| miss "default branch master no longer exists"
# 4. Critical files exist
test -f "lib/paper_trail.rb" \\
&& ok "lib/paper_trail.rb" \\
|| miss "missing critical file: lib/paper_trail.rb"
test -f "lib/paper_trail/has_paper_trail.rb" \\
&& ok "lib/paper_trail/has_paper_trail.rb" \\
|| miss "missing critical file: lib/paper_trail/has_paper_trail.rb"
test -f "lib/paper_trail/record_trail.rb" \\
&& ok "lib/paper_trail/record_trail.rb" \\
|| miss "missing critical file: lib/paper_trail/record_trail.rb"
test -f "lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb" \\
&& ok "lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb" \\
|| miss "missing critical file: lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb"
test -f "lib/paper_trail/reifier.rb" \\
&& ok "lib/paper_trail/reifier.rb" \\
|| miss "missing critical file: lib/paper_trail/reifier.rb"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 31 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~1d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/paper-trail-gem/paper_trail"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
PaperTrail is a Ruby gem that tracks changes to Rails model attributes, creating an immutable audit log stored in a versions database table. It captures who changed what, when, and why—enabling you to revert models to any historical state, restore deleted records, and query the complete change history of any object. Modular gem structure: lib/paper_trail/ contains core logic organized by concern (events, attribute serializers, configuration). lib/generators/ provides Rails generators for initial setup and migrations. lib/paper_trail/events/ (base.rb, create.rb, etc.) isolates lifecycle event handling. Migrations are template-driven via gemfiles/ and the install generator.
👥Who it's for
Rails developers building applications that require audit trails, compliance reporting, or undo/revert functionality—particularly those in regulated industries (finance, healthcare) or building content management systems where change history is critical.
🌱Maturity & risk
Production-ready and actively maintained. The gem shows 364k lines of Ruby code, comprehensive test coverage via RSpec, automated CI/CD via GitHub Actions (see .github/workflows/test.yml), and supports Rails 7.1 through 8.1 (via gemfiles/ appraisals). Version 17.0 is current; the codebase is well-documented with versioned README snapshots dating back to v1.6.
Low risk for stable features, but moderate risk around serialization: the gem handles YAML deserialization (see doc/pt_13_yaml_safe_load.md), which historically had security implications. Dependency load is light (standard Rails ecosystem). Single-maintainer governance is a minor concern, but the issue/PR template maturity suggests active triage (.github/ISSUE_TEMPLATE/). Breaking changes are documented in CHANGELOG.md.
Active areas of work
Active development across Rails 8.1 compatibility (gemfiles/rails_8.1.gemfile exists alongside older versions). Recent work focuses on attribute serialization robustness (lib/paper_trail/attribute_serializers/) and YAML safety. Stale issue automation is enabled (.github/workflows/stale.yml).
🚀Get running
git clone https://github.com/paper-trail-gem/paper_trail.git
cd paper_trail
bundle install
rake test
Daily commands:
This is a library, not an app. To run tests: bundle exec rspec or rake test. To integrate into a Rails app: rails generate paper_trail:install to create migrations, then add has_paper_trail to model classes.
🗺️Map of the codebase
lib/paper_trail.rb— Main entry point and module loader that defines the PaperTrail API and configuration hooks.lib/paper_trail/has_paper_trail.rb— Core mixin that adds version tracking to any Rails model; essential for understanding how tracking is attached.lib/paper_trail/record_trail.rb— Handles the creation and management of version records during model lifecycle events.lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb— The Version model that stores all historical state; central to querying and restoring versions.lib/paper_trail/reifier.rb— Reconstructs model instances from version data; critical for restore and historical queries.lib/paper_trail/events/base.rb— Base event handler for create, update, destroy operations that capture changes.lib/paper_trail/config.rb— Configuration management for PaperTrail settings including serializers and global options.
🛠️How to make changes
Add tracking to a new model
- Open your model file and add the has_paper_trail() call (
spec/dummy_app/app/models/article.rb) - Run the install generator if you haven't already to create the versions table (
lib/generators/paper_trail/install/install_generator.rb) - Configure tracking options via has_paper_trail(only: [...], skip: [...], meta: {...}) in the model (
lib/paper_trail/has_paper_trail.rb)
Query version history and restore state
- Use model.versions to access the ActiveRecord association of version records (
lib/paper_trail/record_history.rb) - Use where_object and where_object_changes queries to find versions by state (
lib/paper_trail/queries/versions/where_object.rb) - Call version.reify to reconstruct the model at that point in time (
lib/paper_trail/reifier.rb)
Set request context (whodunnit) for audit tracking
- In your Rails controller, include the PaperTrail controller mixin (
lib/paper_trail/frameworks/rails/controller.rb) - Call set_paper_trail_whodunnit in a before_action or set PaperTrail.request.whodunnit directly (
lib/paper_trail/request.rb) - Access version.whodunnit in version records to see who made each change (
lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb)
Customize serialization format
- Edit lib/paper_trail/config.rb or set PaperTrail.config.serializer at runtime (
lib/paper_trail/config.rb) - Choose JSON or YAML or implement a custom serializer matching the interface (
lib/paper_trail/serializers/json.rb) - The attribute serializer factory will use your choice when encoding/decoding object and object_changes (
lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb)
🔧Why these technologies
- ActiveRecord Callbacks (after_create, after_update, after_destroy) — Non-invasive hook into Rails model lifecycle without modifying core Rails code.
- Serialization (JSON/YAML) for object and object_changes — Stores arbitrary model state in a database-agnostic way; flexible format for restoration and queries.
- Thread-local storage (request context) — Captures whodunnit/metadata across a request without passing context through entire call stack.
- Rails generators for migrations — Standardizes installation; avoids requiring manual schema setup.
⚖️Trade-offs already made
-
Store full serialized object + object_changes separately
- Why: Reduces complexity of diffing and reification; allows efficient queries on specific attribute changes.
- Consequence: Increased storage footprint; requires careful serialization strategy to avoid bloat.
-
Use callbacks instead of wrapping ActiveRecord::Relation#update_all
- Why: Simpler implementation, works with all save patterns.
- Consequence: Bulk updates bypass callbacks; not tracked unless wrapped explicitly.
-
Thread-local request context instead of explicit parameter passing
- Why: Avoids plumbing whodunnit through models and services.
- Consequence: Hidden dependency; can cause issues in async/threaded contexts if not cleared properly.
-
Single versions table for all models with item_type/item_id polymorphism
- Why: Simpler queries across all versions; easier to build global audit logs.
- Consequence: Large single table; may need indexing strategy for high-volume apps.
🚫Non-goals (don't propose these)
- Does not track associations (has_many, has_one, HABTM) automatically; only model attributes.
- Does not provide real-time change notifications or webhooks.
- Does not support automatic conflict resolution in multi-user edits.
- Does not handle encryption of sensitive data in versions (user must serialize before storing).
- Not a temporal query engine like PostgreSQL temporal tables; requires manual reification.
🪤Traps & gotchas
- The
whodunnitthread-local variable must be set manually unless usingPaperTrail.request.whodunnit=in controller (seedoc/warning_about_not_setting_whodunnit.md). 2) YAML deserialization usesPsych.safe_load(Rails 5.1+) to prevent code injection—verify Rails version matches gem expectations. 3) Theobject_changescolumn requires explicit migration viarails generate paper_trail:install:migrationfor Rails 5.0+; it's not created by default in older setups. 4) Custom serializers must implementdumpandloadmatching the factory interface inattribute_serializer_factory.rb.
🏗️Architecture
💡Concepts to learn
- Optimistic Locking & Versioning — PaperTrail's core pattern: instead of preventing concurrent writes, it records every state change immutably; understanding versioning vs. locking is essential to design correct audit trails
- Serialization Strategy Pattern — The gem delegates attribute serialization to pluggable serializers (YAML, JSON, Cast) via
AttributeSerializerFactory; critical to understand for custom type handling or migration between formats - Thread-Local Storage (RequestStore) — PaperTrail uses
PaperTrail.request.whodunnitto store per-request context (user ID, etc.) without polluting arguments; essential for multi-threaded Rails servers - ActiveRecord Callbacks & Hooks — PaperTrail relies on
before_save,after_create,after_update,after_destroyto trigger version recording; understanding callback ordering is critical to prevent double-recording - Event Sourcing (Immutable Audit Log Pattern) — PaperTrail implements event sourcing principles: every change is an immutable event; you can reconstruct any past state by replaying events—foundational to understanding the gem's design
- Polymorphic Associations — PaperTrail's
Versionmodel usesitem_typeanditem_idto store changes for any model; understanding polymorphic queries is required for filtering versions across models - YAML Deserialization Security — Mentioned in
doc/pt_13_yaml_safe_load.md: unsafe YAML deserialization can execute arbitrary code; PaperTrail usesPsych.safe_loadto prevent injection attacks on stored object history
🔗Related repos
audited/audited— Direct competitor: similar audit trail gem for Rails withauditedmacro; compares on feature set and performancechangelogs/changelog.md— Not a repo, but PaperTrail users often referenceCHANGELOG.mdfor breaking changes; consider reviewing real audit trail alternatives like Auditedrails/rails— Core dependency; PaperTrail integrates deeply with ActiveRecord callbacks and migrations (supports Rails 7.1–8.1)ruby/psych— Serialization dependency for YAML handling; PaperTrail relies on safe YAML loading to prevent code injection vulnerabilitiesthoughtbot/paranoia— Soft delete gem often paired with PaperTrail; users combine them for deleted record recovery with version history
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add comprehensive test coverage for attribute serializers
The lib/paper_trail/attribute_serializers/ directory contains critical serialization logic (object_attribute.rb, object_changes_attribute.rb, cast_attribute_serializer.rb) but there's a README.md suggesting this area needs documentation. A new contributor could add detailed unit tests covering edge cases like nil values, type casting failures, and serialization/deserialization round-trips for different Rails attribute types.
- [ ] Create spec/paper_trail/attribute_serializers/ test directory structure
- [ ] Add tests for lib/paper_trail/attribute_serializers/object_attribute.rb covering serialization of various Ruby types
- [ ] Add tests for lib/paper_trail/attribute_serializers/object_changes_attribute.rb with attribute change tracking edge cases
- [ ] Add tests for lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb testing type coercion
- [ ] Ensure tests cover the attribute_serializer_factory.rb factory pattern with different Rails versions (7.1-8.1)
Expand event-based versioning tests across Rails 7.1-8.1
The lib/paper_trail/events/ directory has base.rb, create.rb, destroy.rb, and update.rb event classes, but these need robust testing across the supported gemfiles (rails_7.1, 7.2, 8.0, 8.1). A new contributor could add integration tests that verify event handling works correctly in each Rails version, ensuring backwards compatibility.
- [ ] Create spec/paper_trail/events/ test directory for event-specific tests
- [ ] Add integration tests for create events using Appraisals across all Rails versions
- [ ] Add integration tests for update events with various attribute change scenarios
- [ ] Add integration tests for destroy events ensuring proper version recording
- [ ] Add tests for event serialization/deserialization with object_changes data
Add missing integration tests for Version model queries
The lib/paper_trail/queries/versions directory exists but appears incomplete. Create comprehensive tests for the Version model's query capabilities (filtering, scoping, ordering) that mirror real-world audit trails usage patterns, ensuring the query API is well-documented and bug-free.
- [ ] Explore lib/paper_trail/queries/versions to understand existing query classes
- [ ] Create spec/paper_trail/queries/ test directory structure
- [ ] Add tests for version filtering by version type (create/update/destroy)
- [ ] Add tests for version scoping by item_type and item_id across associations
- [ ] Add tests for version ordering and pagination to support audit trail UI requirements
🌿Good first issues
- Add comprehensive tests for
PaperTrail::AttributeSerializers::CastAttributeSerializerin new spec files matching the factory pattern; currentlylib/paper_trail/attribute_serializers/lacks co-located test coverage. - Expand
doc/folder with a migration guide for upgrading from v16 to v17 highlighting serialization changes; onlydoc/pt_13_yaml_safe_load.mdexists as a versioning edge case, but no v16→v17 upgrade doc. - Add generator test scenarios in
spec/for theupdate_item_subtypegenerator (lib/generators/paper_trail/update_item_subtype/); the template exists but integration tests for STI (single table inheritance) model versioning are sparse.
⭐Top contributors
Click to expand
Top contributors
- @jaredbeck — 48 commits
- @fatkodima — 11 commits
- @modosc — 7 commits
- @hatsu38 — 5 commits
- @aried3r — 4 commits
📝Recent commits
Click to expand
Recent commits
098058a— Fix rubocop errors in CI (#1554) (skipkayhil)95a34d0— CI: Add Ruby 4.0 to CI Matrix (#1547) (unochin)0c511ed— Release 17.0.0 (fatkodima)073cc30— Make support for Rails 8.1 official (#1538) (leoarnold)f19dd80— Add rails 8.1.beta to Appraisals (#1530) (modosc)1686e18— feat: add argument for custom version table generation (#1528) (BedeDD)b1abd5c— Rubocop fixes (#1531) (modosc)cae7f5d— Fix styles for the latest rubocop (fatkodima)a60a451— Remove unused gemfiles for older rails (fatkodima)e1a4cde— Fix badges in README.md (#1517) (modosc)
🔒Security observations
PaperTrail is a mature auditing gem with generally solid security practices, but has some areas of concern. The primary risk is improper YAML deserialization which could lead to RCE if unsafe methods are used. Secondary risks include potential SQL injection in query builders and information disclosure via unfiltered version history (particularly sensitive data tracking). The gem's core purpose of storing all object changes creates inherent risks around storing sensitive information. Developers using this gem must explicitly configure exclusions for sensitive attributes and implement proper authorization controls. The codebase appears well-maintained with awareness of YAML safety issues, suggesting these have been addressed, but code review of serialization implementations is recommended.
- High · Potential YAML Deserialization Vulnerability —
lib/paper_trail/serializers/yaml.rb and lib/paper_trail/attribute_serializers/. The codebase includes a YAML serializer (lib/paper_trail/serializers/yaml.rb) which could be vulnerable to arbitrary code execution if unsafe YAML deserialization is used. The documentation references 'doc/pt_13_yaml_safe_load.md' suggesting this was a known issue. Unsafe YAML.load() can deserialize arbitrary Ruby objects leading to RCE. Fix: Ensure YAML.safe_load() is used exclusively instead of YAML.load(). Verify all YAML deserialization in the serializer and attribute handling uses safe loading with appropriate permitted_classes configuration. - Medium · SQL Injection Risk in Query Building —
lib/paper_trail/queries/versions/where_attribute_changes.rb, where_object.rb, where_object_changes.rb, where_object_changes_from.rb, where_object_changes_to.rb. The codebase contains multiple query files (lib/paper_trail/queries/versions/) that build dynamic queries. If user input is interpolated into SQL queries without proper parameterization, SQL injection vulnerabilities could arise. Fix: Audit all query construction to ensure parameterized queries are used. Avoid string interpolation with user input. Use ActiveRecord's query interface with bound parameters (? placeholders). - Medium · Object Deserialization in Reifier —
lib/paper_trail/reifier.rb. The lib/paper_trail/reifier.rb file handles restoring/reifying objects from stored versions. If attribute serialization is not properly validated, this could lead to object injection vulnerabilities or unexpected state restoration. Fix: Implement strict type validation when reifying objects. Ensure deserialized attributes match expected types. Validate that restored objects don't bypass security constraints or authorization checks. - Medium · Missing Input Validation in Record Trail —
lib/paper_trail/record_trail.rb, lib/paper_trail/record_history.rb. The record_trail.rb and record_history.rb files manage audit trail recording. If user-supplied metadata (like 'whodunnit') is stored without validation, it could be exploited for log injection or stored XSS attacks. Fix: Validate and sanitize all user-supplied metadata before storing in version records. Implement strict type checking for 'whodunnit' and other user inputs. Consider escaping special characters. - Low · Potential Information Disclosure via Version History —
lib/paper_trail/model_config.rb, lib/paper_trail/has_paper_trail.rb. PaperTrail stores complete object state changes. Sensitive attributes (passwords, tokens, PII) may be exposed in version records if not explicitly excluded. The gem stores these in the database potentially accessible to database users or backups. Fix: Implement and enforce attribute exclusion lists (skip, only, ignore options). Document which sensitive attributes should never be tracked. Consider encryption for sensitive version data at rest. Implement proper access controls on versions table. - Low · Missing Security Headers Documentation —
lib/paper_trail/frameworks/rails/controller.rb. As a Rails gem, the framework integration (lib/paper_trail/frameworks/rails/) may not document security best practices for controller integration, particularly around authorization checks for viewing version history. Fix: Add documentation requiring developers to implement authorization checks before exposing version history endpoints. Provide example code for secure controller implementations. Consider adding helper methods for common authorization patterns.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.