TL;DR
Short of monkey-patching Kernel#warn (see https://stackoverflow.com/a/662436/1301972) to raise an exception, you won't be able to prevent reassignment to the constant itself. This is generally not a pragmatic concern in idiomatic Ruby code where one expects to be able to do things like reopen classes, even though class names are also constants.
A Ruby constant isn't actually immutable, and you can't freeze a variable. However, you can get an exception to be raised when something attempts to modify the contents of a frozen object referenced by the constant.
Freezing Objects Deeply with Plain Ruby
Freezing an Array is easy:
CONSTANT_ONE = %w[one two three].freeze
but the strings stored in this Array are really references to String objects. So, while you can't modify this Array, you can still (for example) modify the String object referenced by index 0. To solve this problem, you need to freeze not just the Array, but the objects it holds, too. For example:
CONSTANT = %w[one two three].map(&:freeze).freeze
CONSTANT[2] << 'four'
# RuntimeError: can't modify frozen String
CONSTANT << 'five'
# RuntimeError: can't modify frozen Array
Freezing Objects Recursively with a Gem
Since freezing recursive references can be a bit unwieldy, it's good to know there's a gem for that. You can use ice_nine to deep-freeze most objects:
require 'ice_nine'
require 'ice_nine/core_ext/object'
OTHER_CONST = %w[a b c]
OTHER_CONST.deep_freeze
OTHER_CONST << 'd'
# RuntimeError: can't modify frozen Array
OTHER_CONST[2] = 'z'
# RuntimeError: can't modify frozen Array
A Better Way to Use Ruby Constants
Another option to consider is calling Object#dup when assigning the value of a constant to another variable, such as instance variables in your class initializers, in order to ensure you don't mutate your constant's references by accident. For example:
class Foo
CONSTANT = 'foo'
attr_accessor :variable
def initialize
@variable = CONSTANT.dup
end
end
foo = Foo.new
foo.variable << 'bar'
#=> "foobar"
Foo::CONSTANT
#=> "foo"